IBBA Partner API v1
ILN card crypto topup — deposit wallets, money flow, and webhooks
Base URL: https://partner-api.ibba.group/api/partner/v1
Doc updated 2026-09-08 — live POST /topups fixed and verified end to end;
corrected what the API actually exposes today (fixed wallets, webhooks, expiry).
Contents
Deposit wallets Money flow Webhooks Auth Endpoints Create topup Fees Sandbox ErrorsDeposit wallets — how you get one
POST /topups for a card, and comes back in that response as
deposit_address. Nothing has to be requested or provisioned in advance.
Fixed per card The address is permanent for that card and network.
Later calls for the same card return the same address, it does not expire, and
you may cache it. GET /topups/{order_id} returns it again at any time.
Idempotent Re-sending the same order_id returns the
existing topup — it never creates a second one.
The address does not identify the order. Two open topups on the same card share one address, so a deposit is matched to an order by network + exact amount, newest first, within a 48 h window. Send the exact amount: do not round it and do not batch several topups into one transfer. If nothing matches, the deposit is held for manual review rather than credited to a guess.
How the address is provisioned
- Cardholder has an Interlace card (
issuer_id = 3) in CardHub - Your first
POST /topupsfor that card creates a dedicated wallet, binds it to the card's Interlace UUID and stores the address — automatically, no IBBA intervention - ILN shows that address in their topup UI and reuses it forever
- Payer sends USDT → Ezequiel detects → sweeps → credits the Interlace card
topup.completedfires with the final amounts
Typical integration
GET /cardholders/{email}/cards— pick an Interlace cardPOST /topups— receivedeposit_address,chainand fees- Payer funds it with the exact amount; wait for
topup.completedor poll
Issuer rails (important)
| Issuer | issuer_id | Live crypto → card | Notes |
|---|---|---|---|
| Interlace | 3 | Yes | Fixed-wallet hybrid + Partner live topups |
| Versatec | 1 | Out of scope for this API | Versatec cards keep using the existing manual topup flow, unchanged. POST /topups returns issuer_not_supported_for_crypto_topup for them |
Money flow (end-to-end)
A) Fixed wallet — provisioned automatically by your first POST /topups
B) POST /topups (CN-01) — the integration path
Step-by-step
| # | Step | Who | Partner sees |
|---|---|---|---|
| 1 | Create topup → deposit wallet allocated | Partner → API → Quikipay | POST /topups response: status=pending + deposit_address |
| 2 | USDT sent on-chain to that address | Payer | (nothing yet — keep polling or wait for webhook) |
| 3 | Deposit detected & confirmed on NODE rail | Ezequiel / Quikipay | Internal; partner status still pending until card credit |
| 4 | Fees applied (rail + network + conversion) | Quikipay / IBBA | Final amounts in topup.completed |
| 5 | USD credited to Interlace card | CH-API → Interlace | status flips to completed — visible on GET /topups/{order_id} and GET /events; webhook only if a webhook_url is registered |
| 6 | Order never funded | — | Nothing. It simply stays pending; live topups are not expired automatically. The address stays valid, so a later payment for the exact amount still matches within 48 h. |
Do not send real funds to sandbox addresses. Live addresses are fixed per card and network — shared by every topup on that card, which is why the exact amount is what identifies the payment.
Webhooks & events
A webhook URL is registered on the ILN live credential
(https://iln.law/card/webhook), so deliveries are on. You can still integrate by
polling GET /events?since={last_id} or GET /topups/{order_id} — both are
populated whether or not webhooks are on, and polling is the safer belt-and-braces option while
you build. To change the URL, send it to IBBA.
We POST JSON:
X-Partner-Event: topup.completed X-Partner-Timestamp: <unix seconds> X-Partner-Signature: HMAC-SHA256( timestamp + "\n" + raw_body, secret ) // hex lowercase Content-Type: application/json
Partner-facing events (what you integrate)
| Event | When | Meaning |
|---|---|---|
topup.completed | After Interlace card credit succeeds | Funds settled to card — use net_credited / fee breakdown |
topup.expired | Sandbox only. Fired by POST /sandbox/topups/{order_id}/simulate. Nothing emits it on live today. | Do not build a live flow that waits for this — treat a topup still pending after expires_at as abandoned |
topup.completed, poll GET /topups/{order_id} (status stays pending) or GET /events.
topup.completed payload (example)
{
"event": "topup.completed",
"order_id": "iln-order-001",
"status": "completed",
"cardholder_email": "cardholder@example.com",
"card_id": 12345,
"amount": "100.00",
"currency": "USD",
"network": "ERC20",
"sandbox": false,
"fee_breakdown": {
"gross_amount": "100.00",
"rail_fee": "1.00",
"network_fee": "2.00",
"conversion_fee": "0.10",
"fees_total": "3.10",
"estimated_net_credit": "96.90"
},
"net_credited": "96.90",
"gross_amount": "100.00",
"fees_total": "3.10"
}
Internal pipeline (for ops understanding — not partner webhooks)
| Internal step | System | Partner webhook? |
|---|---|---|
| CN-01 order + deposit address allocated | Quikipay NODE | No — returned in POST /topups |
| On-chain deposit detected | Ezequiel (Turnkey EVM / TRC20 watcher) | No |
| QP deposit notify pending → completed | Quikipay → /api/depositIntoQuikipayILN | No |
| Card credit Interlace | CH-API | Yes → topup.completed |
Polling — the path to build on today
GET /events?since={last_id} — cursor feed of the same events. Start at
since=0, then pass back the next_cursor from each response. Events are
recorded whether or not a webhook URL is configured, so this works right now; a single topup can
also just be re-read with GET /topups/{order_id} until status is
completed.
Sandbox: POST /sandbox/webhooks/replay re-sends the last event to your configured URL.
Authentication (HMAC)
Every request must include:
| Header | Description |
|---|---|
X-Partner-Key | Your API key |
X-Partner-Timestamp | Unix seconds (max skew 300s) |
X-Partner-Signature | HMAC-SHA256( timestamp + "\n" + raw_body, secret ) hex lowercase |
For GET requests the body is the empty string.
signature = HMAC_SHA256( timestamp + "\n" + body, secret )
Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /cardholders/{email}/cards | List cards (includes issuer / issuer_id) |
| POST | /topups | Create topup + deposit wallet (idempotent by order_id) |
| GET | /topups/{order_id} | Get topup status + address |
| GET | /topups | List topups (paginated) |
| GET | /events?since=0 | Poll events (cursor = last id) |
| POST | /sandbox/topups/{order_id}/simulate | Sandbox: completed or expired |
| POST | /sandbox/webhooks/replay | Sandbox: replay last webhook |
Create topup (= create deposit wallet)
POST /topups
Content-Type: application/json
{
"order_id": "iln-test-001",
"cardholder_email": "cardholder@example.com",
"card_id": 12345,
"amount": 100.00,
"network": "ERC20"
}
| Field | Required | Notes |
|---|---|---|
order_id | Yes | Unique per partner credential; reuse returns existing topup |
cardholder_email | Yes | Must belong to your company |
card_id | Yes | From cards list — prefer Interlace for live crypto→card |
amount | Yes | Gross USD; minimum usually 50.00 |
network | No | ERC20 (default) or TRC20 |
Success response (wallet fields highlighted)
{
"success": true,
"topup": {
"order_id": "iln-test-001",
"status": "pending",
"cardholder_email": "cardholder@example.com",
"card_id": 12345,
"amount": "100.00",
"currency": "USD",
"network": "ERC20",
"sandbox": false,
"deposit_address": "0xA327Ae6Fb7529c7bE9073f174F58f2659Ae5ABb2",
"chain": "ETH",
"qr_code": "https://panel.quikipay.com/storage/qr_codes/d69460f3…bce8.png",
"fee_breakdown": {
"gross_amount": "100.00",
"rail_fee": "1.00",
"network_fee": "2.00",
"conversion_fee": "0.10",
"fees_total": "3.10",
"estimated_net_credit": "96.90",
"note": "Estimado. El valor final se informa en topup.completed."
},
"expires_at": "2026-08-26T15:21:47+00:00",
"completed_at": null,
"created_at": "2026-08-26T14:36:47+00:00"
}
}
deposit_address— the wallet to fund (ERC20 = ETH address; TRC20 = TRON address)chain—ETHorTRONqr_code— a PNG URL you can render directly;nullin sandboxexpires_at— ~45 min from creation. Nothing changes server-side when it passes: the topup stayspending, so stop waiting and create a neworder_idfee_breakdown.estimated_net_credit— estimate; final intopup.completed
Fees (estimate)
iln retains
1% + 2 USDT txn, then Interlace takes its USDT→USD conversion fee.
Example: 100 USDT → IBBA keeps 3 → 97 USDT to Interlace → card ~96.8 USD after FX.
Cardholder approx: gross − 1% − 2 USDT − Interlace FX.
| Fee | Value |
|---|---|
| IBBA commission (Midaz) | 1% of gross |
| IBBA txn fee (Midaz) | 2 USDT flat |
| Interlace FX | ~0.2% USDT→USD (external) |
Rail (legacy POST /topups) | 1.00% of gross |
| Network (legacy NODE) | USD 2.00 |
Legacy POST /topups example gross USD 100.00 → estimated net ~USD 96.90. Final values arrive in topup.completed.
Sandbox credentials
Sandbox only. Do not use in production. Do not send real crypto to sandbox addresses.
| API Key | pk_sandbox_iln_9c74a2dfb2da |
|---|---|
| Secret | c1c46252638f0aa2dd972f9d3aa00fb76da52eefd1262fda |
| Mode | sandbox |
| Min amount | USD 50.00 |
Sandbox flow
GET /cardholders/{email}/cardsPOST /topups— inspectdeposit_address(dummy)POST /sandbox/topups/{order_id}/simulatebody{"event":"completed"}GET /events?since=0— receivetopup.completed
Example (curl + bash)
BASE="https://partner-api.ibba.group/api/partner/v1"
KEY="pk_sandbox_iln_9c74a2dfb2da"
SECRET="c1c46252638f0aa2dd972f9d3aa00fb76da52eefd1262fda"
TS=$(date +%s)
BODY='{"order_id":"demo-1","cardholder_email":"...","card_id":123,"amount":100,"network":"ERC20"}'
SIG=$(printf '%s\n%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -sS "$BASE/topups" -H "Content-Type: application/json" \
-H "X-Partner-Key: $KEY" -H "X-Partner-Timestamp: $TS" -H "X-Partner-Signature: $SIG" \
-d "$BODY"
# Response includes topup.deposit_address — that is the wallet.
Errors
| HTTP | message |
|---|---|
| 401 | missing_partner_auth_headers, invalid_timestamp, timestamp_expired, invalid_api_key, invalid_signature |
| 403 | ip_not_allowed, sandbox_only |
| 404 | not_found, cardholder_not_found |
| 422 | amount_below_minimum, card_not_found, invalid_network |
| 502 | quikipay_pay_failed, quikipay_order_fetch_failed (live rail) |
Contact IBBA for live credentials and webhook URL registration.
Live keys use the same paths; deposit addresses are real NODE wallets — fund only the address returned for that order_id.