SwiftSub API v2

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).

Base URL: http://127.0.0.1:8000/api/v2Live & SandboxRate: 60 req/min/key

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_live_… (real money)
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

ScenarioOutcomeHTTPWebhook Event
successsuccessful200transaction.successful
ineligibleineligible_line422transaction.ineligible
insufficient_balanceinsufficient_provider_balance422transaction.provider_balance_low
failurefailed422transaction.failed

GET /services — Price list

Active catalogue, Tier-aware pricing.

GET http://127.0.0.1:8000/api/v2/services
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.

{ "status": "success", "data": { "main_balance_ngn": 12450.00, "earning_balance_ngn": 0.00 } }

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-empty every 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.

// Node.js example
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).

OutcomeCustomer MessageAdmin AlertWebhook
successfulConfirmation with amount, network, refLog onlytransaction.successful
ineligible_lineClear reason: number not eligibleLogtransaction.ineligible + INELIGIBLE_LINE
insufficient_provider_balanceGeneric "processing delay" (hide real reason)SYNC WhatsApp admin with real reasontransaction.provider_balance_low
failedGeneric failure + refundedLog full payloadtransaction.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