SwiftSub API v2
B2B reseller API — same two-phase wallet pipeline as the app, plus outgoing webhooks, sandbox test mode, and deterministic classification (successful, ineligible_line, insufficient_provider_balance, failed).
Authentication
Create API key from Pricing & API page (Tier 3 required). Keys are hashed (sha256) — plaintext shown once. Send as Bearer token.
Authorization: Bearer ss_test_… (sandbox — no spend)
Live key
ss_live_… → real VendorRouterService, wallet debited, real provider calls, webhooks fired.
Sandbox key
ss_test_… → SandboxSimulator, NO wallet debit, simulated responses, webhooks still fired for E2E testing.
Sandbox / Test Mode
Minimal flag or separate API key tier that lets developers hit same endpoints with simulated responses without touching real VendorRouterService or spending real provider balance.
# Simulate scenarios via header or query
X-Simulate-Scenario: success | ineligible | insufficient_balance | failure
curl -X POST http://127.0.0.1:8000/api/v2/purchase \
-H "Authorization: Bearer ss_test_abc..." \
-H "X-Simulate-Scenario: ineligible" \
-d '{"service_id":1,"phone":"08012345678"}'
# Also ?simulate=success works
| Scenario | Outcome | HTTP | Webhook Event |
|---|---|---|---|
| success | successful | 200 | transaction.successful |
| ineligible | ineligible_line | 422 | transaction.ineligible |
| insufficient_balance | insufficient_provider_balance | 422 | transaction.provider_balance_low |
| failure | failed | 422 | transaction.failed |
GET /services — Price list
Active catalogue, Tier-aware pricing.
Authorization: Bearer ss_live_…
→ {status: "success", data: [{id, category, provider, type, name, size, unit, price_ngn}, …]}
POST /purchase — Buy service
Request
"service_id": 12, // required
"phone": "08012345678", // data/airtime
"amount": 1000, // airtime/electricity
"meter_number": "12345678901", // electricity
"customer_name": "John Doe", // electricity/cable
"iuc_number": "1234567890", // cable
"quantity": 1, // education
"external_ref": "your-own-ref-123" // optional idempotency
}
Headers (optional but recommended):
Idempotency-Key: your-own-ref-123 // prevents double debit on retry
X-Simulate-Scenario: success // sandbox only
Idempotency: send same Idempotency-Key or external_ref within 24h → original transaction returned, no new debit. Header X-Idempotent-Replayed: true on replay.
Response (success)
"status": "success",
"message": "Data purchase delivered.",
"transaction": {
"reference": "DATA-AB12CD34EF",
"service": "data",
"amount_ngn": 530.00,
"status": "successful",
"recipient": {"phone": "080..."}
}
}
Failure → 422 with error_code: INELIGIBLE_LINE, INSUFFICIENT_PROVIDER_BALANCE, PROVIDER_TIMEOUT, VENDOR_DECLINED, INSUFFICIENT_USER_BALANCE
GET /transactions/{reference}
Lookup status. Works for both live and sandbox transactions.
GET /wallet
Balance check.
Webhooks — Outgoing transaction status
Every transaction result also fires an outgoing webhook to reseller's registered endpoint, using same classification as Part A, with documented JSON payload shape.
Delivery reliability
- Retry failed deliveries with backoff: 60s, 300s, 900s
- Log failures (webhook_deliveries table)
- If 3+ consecutive failures → WhatsApp alert to admin flagging endpoint as unreachable
- Queue via cron:
php artisan queue:work --stop-when-emptyevery minute (cPanel compatible)
Webhook Payload Schema
"event": "transaction.successful", // or transaction.ineligible, transaction.provider_balance_low, transaction.failed
"status": "successful", // successful | ineligible_line | insufficient_provider_balance | failed
"reference": "DATA-AB12CD34EF",
"external_reference": "your-own-ref-123",
"service_type": "data",
"amount": 530.00,
"total_debited": 530.00,
"provider": "mtn",
"recipient": {"phone": "08012345678", "network": "MTN"},
"timestamp": "2026-09-16T12:00:00Z",
"error_code": null, // INELIGIBLE_LINE | INSUFFICIENT_PROVIDER_BALANCE | VENDOR_DECLINED etc.
"is_sandbox": false,
"idempotency_key": "DATA-AB12CD34EF-transaction.successful"
}
Headers:
X-SwiftSub-Signature: <HMAC-SHA256(payload, secret)>
X-SwiftSub-Event: transaction.successful
Webhook Management
POST /api/v2/webhooks
{"url": "https://yourapp.com/webhooks/swiftsub", "events": ["transaction.successful","transaction.failed"] } → returns secret whsec_…
GET /api/v2/webhooks
List your endpoints
DELETE /api/v2/webhooks/{id}
Delete endpoint
POST /api/v2/webhooks/{id}/test
Send test payload
GET /api/v2/webhooks/deliveries
Recent delivery logs (50 latest)
Verifying Signatures
Each webhook includes X-SwiftSub-Signature = HMAC-SHA256(JSON payload, secret). Verify to ensure authenticity.
const crypto = require('crypto');
const sig = req.headers['x-swiftsub-signature'];
const expected = crypto.createHmac('sha256', secret).update(JSON.stringify(req.body)).digest('hex');
if (sig !== expected) throw new Error('Invalid signature');
// PHP
$sig = $_SERVER['HTTP_X_SWIFTSUB_SIGNATURE'];
$expected = hash_hmac('sha256', json_encode($payload), $secret);
if (!hash_equals($expected, $sig)) die('Invalid');
Error Codes & Classification
NotificationBotService classifies using deterministic PHP logic (NOT LLM).
| Outcome | Customer Message | Admin Alert | Webhook |
|---|---|---|---|
| successful | Confirmation with amount, network, ref | Log only | transaction.successful |
| ineligible_line | Clear reason: number not eligible | Log | transaction.ineligible + INELIGIBLE_LINE |
| insufficient_provider_balance | Generic "processing delay" (hide real reason) | SYNC WhatsApp admin with real reason | transaction.provider_balance_low |
| failed | Generic failure + refunded | Log full payload | transaction.failed + VENDOR_DECLINED etc. |
Need help integrating?
Use sandbox keys to test all scenarios without spending. Webhooks retry automatically. Admin gets WhatsApp alert if your endpoint fails 3+ times.
SwiftSub API v2 • Lagos, Nigeria • Built with Laravel • Docs generated from actual routes/controllers
