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 Errors

Deposit wallets — how you get one

There is no wallet endpoint to call, and you do not need one. The deposit address is created automatically the first time you call 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

  1. Cardholder has an Interlace card (issuer_id = 3) in CardHub
  2. Your first POST /topups for that card creates a dedicated wallet, binds it to the card's Interlace UUID and stores the address — automatically, no IBBA intervention
  3. ILN shows that address in their topup UI and reuses it forever
  4. Payer sends USDT → Ezequiel detects → sweeps → credits the Interlace card
  5. topup.completed fires with the final amounts

Typical integration

  1. GET /cardholders/{email}/cards — pick an Interlace card
  2. POST /topups — receive deposit_address, chain and fees
  3. Payer funds it with the exact amount; wait for topup.completed or poll

Issuer rails (important)

Issuerissuer_idLive crypto → cardNotes
Interlace3YesFixed-wallet hybrid + Partner live topups
Versatec1Out of scope for this APIVersatec 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

ILN topup UI Ezequiel (fixed addr) Interlace CardHub card ───────────── ────────────────────── ───────── ──────────── 1. Show fixed TRC20 address (bound to Interlace UUID) 2. Payer sends USDT ───────► detect (TRC20 watcher / Turnkey EVM webhook) sweep → Interlace CryptoConnect transfer-in → card UUID 3. Card balance updates ─────────────────────────────────────► ACTIVE Interlace card 4. ILN notifies cardholder (own system)

B) POST /topups (CN-01) — the integration path

ILN Partner IBBA Partner API Quikipay NODE (CN-01) Chain / Ezequiel Interlace card ───────────── ──────────────── ───────────────────── ─────────────── ────────────── 1. POST /topups ──────────────► create order + fee estimate ► /v1.1/pay CN-01 allocate deposit wallet ◄────────────────────── deposit_address + chain + QR 2. Show address to payer 3. Payer sends USDT ───────────────────────────────────────────────────────────► deposit_address detect (EVM webhook / TRC20 watcher) confirm + settle ◄── deposit webhook (pending → completed) 4. ◄── /api/depositIntoQuikipayILN credit card (Interlace) emit topup.completed 5. ◄── webhook topup.completed or GET /events / GET /topups/{order_id}

Step-by-step

#StepWhoPartner sees
1Create topup → deposit wallet allocatedPartner → API → QuikipayPOST /topups response: status=pending + deposit_address
2USDT sent on-chain to that addressPayer(nothing yet — keep polling or wait for webhook)
3Deposit detected & confirmed on NODE railEzequiel / QuikipayInternal; partner status still pending until card credit
4Fees applied (rail + network + conversion)Quikipay / IBBAFinal amounts in topup.completed
5USD credited to Interlace cardCH-API → Interlacestatus flips to completed — visible on GET /topups/{order_id} and GET /events; webhook only if a webhook_url is registered
6Order 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)

EventWhenMeaning
topup.completedAfter Interlace card credit succeedsFunds settled to card — use net_credited / fee breakdown
topup.expiredSandbox 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
Intermediate chain steps (detected / sweeping / QP pending) are not separate partner webhooks today. Until 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 stepSystemPartner webhook?
CN-01 order + deposit address allocatedQuikipay NODENo — returned in POST /topups
On-chain deposit detectedEzequiel (Turnkey EVM / TRC20 watcher)No
QP deposit notify pending → completedQuikipay → /api/depositIntoQuikipayILNNo
Card credit InterlaceCH-APIYes → 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:

HeaderDescription
X-Partner-KeyYour API key
X-Partner-TimestampUnix seconds (max skew 300s)
X-Partner-SignatureHMAC-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

MethodPathDescription
GET/cardholders/{email}/cardsList cards (includes issuer / issuer_id)
POST/topupsCreate topup + deposit wallet (idempotent by order_id)
GET/topups/{order_id}Get topup status + address
GET/topupsList topups (paginated)
GET/events?since=0Poll events (cursor = last id)
POST/sandbox/topups/{order_id}/simulateSandbox: completed or expired
POST/sandbox/webhooks/replaySandbox: 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"
}
FieldRequiredNotes
order_idYesUnique per partner credential; reuse returns existing topup
cardholder_emailYesMust belong to your company
card_idYesFrom cards list — prefer Interlace for live crypto→card
amountYesGross USD; minimum usually 50.00
networkNoERC20 (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"
  }
}

Fees (estimate)

Hybrid fixed-wallet (preferred): Midaz Core org 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.
FeeValue
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 Keypk_sandbox_iln_9c74a2dfb2da
Secretc1c46252638f0aa2dd972f9d3aa00fb76da52eefd1262fda
Modesandbox
Min amountUSD 50.00

Sandbox flow

  1. GET /cardholders/{email}/cards
  2. POST /topups — inspect deposit_address (dummy)
  3. POST /sandbox/topups/{order_id}/simulate body {"event":"completed"}
  4. GET /events?since=0 — receive topup.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

HTTPmessage
401missing_partner_auth_headers, invalid_timestamp, timestamp_expired, invalid_api_key, invalid_signature
403ip_not_allowed, sandbox_only
404not_found, cardholder_not_found
422amount_below_minimum, card_not_found, invalid_network
502quikipay_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.