Issue and operate cards, white-label, over one REST API
Issue cards for your users, fund them, read transactions, reveal requisites, and receive signed webhooks — under your own brand.
Full API reference →Base URL and authentication
https://api.oplateam.com
Every request carries your API key as a bearer token. Keys are shown once at
creation: sk_test_… for the sandbox, sk_live_… for production.
curl https://api.oplateam.com/v1/balance \
-H "Authorization: Bearer sk_test_..."
Money conventions
- Amounts are strings with two decimals (
"100.00"), never JSON numbers. - Displayed balances round down; your ledger is authoritative at 6 decimal places.
- Mutating card operations require an
Idempotency-Keyheader — retries are safe.
Issue a card
curl -X POST https://api.oplateam.com/v1/cards \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: your-unique-key" \
-H "Content-Type: application/json" \
-d '{"partner_ref": "your-user-42", "first_name": "Ada", "last_name": "Lovelace"}'
Issuance is asynchronous: you get 202 with a pending card and an operation id.
Poll GET /v1/operations/{id}, or just wait for the card.issued webhook.
Card lifecycle
stateDiagram-v2 [*] --> pending: POST /v1/cards pending --> active: issuance succeeds pending --> failed: issuance fails active --> frozen: freeze frozen --> active: unfreeze active --> terminated: terminate frozen --> terminated: terminate failed --> [*] terminated --> [*]
failed and terminated are terminal. Top-ups,
requisites reveal, and spend controls are accepted only while a card is
active or frozen; state changes are limited to the
arrows above. Repeating a transition already completed is idempotent.
Pagination
Every list returns has_more and an opaque next_cursor;
pass it back as cursor to fetch the next page. A malformed cursor is a
400 invalid_cursor. /v1/transactions and
/v1/events page oldest-first on an append-only sequence and
always return next_cursor on a non-empty page — it is
your resume checkpoint for the next poll. /v1/cards and
/v1/deposits page newest-first and return next_cursor: null
on the last page, so a while next_cursor loop terminates.
Webhooks
Register an endpoint and we push signed events. The full set —
card.issued, card.issue_failed, card.funded,
card.funding_failed, card.state_changed,
card.transaction, deposit.credited,
balance.low — is documented with payload schemas in the
reference under Webhooks. Every event
is also readable from GET /v1/events (cursor-paginated replay log), so a
missed delivery is never lost.
curl -X PUT https://api.oplateam.com/v1/webhook-endpoint \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{"url": "https://your-app.example/hooks/oplateam"}'
# → returns your signing secret (whsec_...) exactly once
Verify the X-Oplateam-Signature: t=<unix>,v1=<hex> header:
# Python
import hashlib, hmac, time
def verify(secret: str, body: bytes, header: str, tolerance: int = 300) -> bool:
try:
parts = dict(p.split("=", 1) for p in header.split(","))
timestamp = int(parts["t"])
signature = parts["v1"]
except (KeyError, ValueError):
return False
if abs(int(time.time()) - timestamp) > tolerance:
return False
expected = hmac.new(secret.encode(),
f"{timestamp}.".encode() + body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
Deduplicate on the event id; order by seq. Delivery is
at-least-once with retries over ~21 hours.
Reveal card details to your user
curl -X POST https://api.oplateam.com/v1/cards/{card_id}/reveal \
-H "Authorization: Bearer sk_test_..."
# → {"object": "reveal_link", "url": "https://api.oplateam.com/r/rvl_...", "expires_at": "..."}
Hand the link to your user through your own channel. It is single-use and expires in 5 minutes; the page shows full card details with no OplaTeam or third-party branding. Deliver it on click (not in an email body — link scanners consume single-use links). The hosted claim page is outside the partner REST API; the API contract ends with the reveal-link response above.
Errors
One envelope everywhere:
{"error": {"type": "invalid_request_error", "code": "card_not_active",
"message": "Card crd_... is not active"}}
| code | meaning |
|---|---|
unauthorized / forbidden | bad key / key lacks access |
validation_error | malformed request — also the shape of every 422; param names the offending field |
invalid_cursor | malformed or incompatible pagination cursor |
idempotency_conflict | same key, different payload |
insufficient_partner_balance | top-up exceeds your balance |
card_terminated / card_not_active | card state refuses the operation |
provider_error / provider_capacity_unavailable | upstream issue — retry later |
Each endpoint's reference entry declares exactly which failure statuses it can produce, all in this envelope.
Rate limits
None are currently enforced. HTTP 429 (envelope type:
rate_limit_error) is reserved — treat it as retryable
with backoff from day one and a future limit will not break your integration.
Test mode
Everything above works with a sk_test_ key against a simulated provider —
no real money, same wire shapes. Events in test mode currently carry
"mode": "live" (a documented limitation, changing with the persistent sandbox).