OplaTeam · Card Issuing API

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

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"}}
codemeaning
unauthorized / forbiddenbad key / key lacks access
validation_errormalformed request — also the shape of every 422; param names the offending field
invalid_cursormalformed or incompatible pagination cursor
idempotency_conflictsame key, different payload
insufficient_partner_balancetop-up exceeds your balance
card_terminated / card_not_activecard state refuses the operation
provider_error / provider_capacity_unavailableupstream 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).

Complete endpoint reference →