Prism Bank API

A REST API over your own Prism Bank data — accounts, transfers, cards, loans, invoices, gateway payments, and real-time webhooks. This page is public; no sign-in required to read it.

Quickstart

Three steps: create a key, make a call, read the response.

1. Create a key

Sign in and open /dashboard/developer. Give it a name, pick at least the accounts:readscope, and copy the raw key — it's shown exactly once.

2. Make your first call

List your own accounts:

curl
curl https://www.prism-bank.com/api/public/v2/accounts \
  -H "Authorization: Bearer pb_live_..."

3. Read the response

Every response — success or failure — has the same envelope shape. See the envelope for the full shape.

200 OK
{
  "success": true,
  "data": {
    "object": "list",
    "data": [
      {
        "object": "account",
        "id": "cmabcd1234",
        "accountNumber": "PB-8K2N4Q",
        "nickname": "Everyday Checking",
        "type": "PERSONAL",
        "status": "ACTIVE",
        "balance": "482.13",
        "createdAt": "2026-01-15T09:30:00.000Z"
      }
    ],
    "has_more": false
  }
}

That's it. Every other endpoint follows the same pattern.

Authentication

Every request is authenticated with a Bearer API key — no sessions, no cookies, no OAuth dance.

Authorization: Bearer pb_live_xxxxxxxxxxxxxxxxxxxxxxxx

Creating and revoking keys

Keys are created and revoked from /dashboard/developer — that page requires signing in, since it's where your actual account access lives. This docs site never creates keys or shows account data; it only explains how to use them.

The raw key is shown exactly once, at creation. Only a hashed form is ever stored — if you lose it, revoke it and create a new one.

Scopes

Each key is granted one or more scopes at creation. A request whose key lacks the scope an endpoint requires gets a permission_error (403), not a silent failure.

ScopeGrants
accounts:readList and view your bank accounts.
transactions:readView an account's transaction history.
transfers:writeMove money between your own accounts, or to another member. Also approves payment requests.
gateway:writePay a merchant through the gateway.
cards:readList your cards.
loans:readList your loans.
invoices:readList invoices for businesses you own.
statements:readReserved for future statement endpoints.
webhooks:manageReserved — webhook endpoints are currently managed only through the dashboard, not the API.

Core concepts

The envelope

Every response — 2xx or otherwise — is JSON with the same top-level shape.

Success
{
  "success": true,
  "data": "..."
}
Failure
{
  "success": false,
  "error": {
    "type": "business_rule_error",
    "code": "insufficient_funds",
    "message": "Insufficient balance."
  }
}

Check success first. Money amounts are always decimal strings ("1234.56"), never floats or integer cents. Timestamps are always ISO-8601. Every resource carries an object field naming what it is.

Errors

The HTTP status and error.type always agree — you can safely branch on either.

TypeStatusWhen
authentication_error401Missing, malformed, invalid, or revoked API key.
permission_error403The key's scopes don't include what this endpoint needs, or the resource exists but isn't yours.
invalid_request_error400Malformed JSON, a validation failure, or an unsupported HTTP method.
not_found_error404The resource doesn't exist at all.
business_rule_error422Understood, but rejected — insufficient funds, an idempotency key reused with a different request, etc.
rate_limit_error429Too many requests for this key in the current window.
api_error500Something unexpected went wrong on our end. Safe to retry.

Rate limits

Each key has a fixed per-minute limit. Every response — success or failure — carries these headers:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 1768470960

X-RateLimit-Reset is a Unix timestamp for when the window rolls over. A 429 also includes Retry-After (seconds).

Idempotency

Every mutating endpoint (transfers, gateway payments, approving a payment request) honors an Idempotency-Key header. Send the same key on a retry — a dropped connection, a timeout — and you get back the exact original response, not a second transfer.

curl
curl https://www.prism-bank.com/api/public/v2/transfers \
  -H "Authorization: Bearer pb_live_..." \
  -H "Idempotency-Key: 8f14e45f-...-my-retry-id" \
  -H "Content-Type: application/json" \
  -d '{"fromAccountId":"acct_1","toAccountId":"acct_2","amount":"50.00"}'

Reusing a key with a differentrequest body is treated as a mistake, not a retry — you'll get a 422 idempotency_key_conflict instead of either request silently winning. Keys are scoped to your own API key, so reusing a value another developer also happened to pick is never a collision. Generate a fresh key (a UUID works well) per logical operation, not per HTTP attempt.

Endpoint reference

Every v2 endpoint. All reads are paginated where they return a list — see Pagination.

Accounts

GET/accountsaccounts:read

List your bank accounts.

Request
curl https://www.prism-bank.com/api/public/v2/accounts \
  -H "Authorization: Bearer pb_live_..."
Response
{
  "success": true,
  "data": {
    "object": "list",
    "data": [
      {
        "object": "account",
        "id": "cmabcd1234",
        "accountNumber": "PB-8K2N4Q",
        "nickname": "Everyday Checking",
        "type": "PERSONAL",
        "status": "ACTIVE",
        "balance": "482.13",
        "createdAt": "2026-01-15T09:30:00.000Z"
      }
    ],
    "has_more": false
  }
}
GET/accounts/{id}accounts:read

Get one account. 404 if it doesn't exist, 403 if it exists but isn't yours.

Request
curl https://www.prism-bank.com/api/public/v2/accounts/cmabcd1234 \
  -H "Authorization: Bearer pb_live_..."
Response
{
  "success": true,
  "data": {
    "object": "account",
    "id": "cmabcd1234",
    "accountNumber": "PB-8K2N4Q",
    "nickname": "Everyday Checking",
    "type": "PERSONAL",
    "status": "ACTIVE",
    "balance": "482.13",
    "createdAt": "2026-01-15T09:30:00.000Z"
  }
}
GET/accounts/{id}/transactionstransactions:read

List an account's ledger history, most recent first.

Request
curl "https://www.prism-bank.com/api/public/v2/accounts/cmabcd1234/transactions?limit=25" \
  -H "Authorization: Bearer pb_live_..."
Response
{
  "success": true,
  "data": {
    "object": "list",
    "data": [
      {
        "object": "transaction",
        "id": "txn_1",
        "type": "TRANSFER_IN",
        "status": "COMPLETED",
        "amount": "50.00",
        "balanceAfter": "482.13",
        "description": "Transfer from Savings",
        "createdAt": "2026-01-15T09:30:00.000Z"
      }
    ],
    "has_more": false
  }
}

Transfers

POST/transferstransfers:writeidempotent

Move money — between two of your own accounts (toAccountId), or to another member (toAccountNumber). Provide exactly one.

Request
curl https://www.prism-bank.com/api/public/v2/transfers \
  -H "Authorization: Bearer pb_live_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"fromAccountId":"cmabcd1234","toAccountNumber":"PB-9Z1X2Y","amount":"25.00","memo":"Lunch"}'
Response
{
  "success": true,
  "data": {
    "object": "transfer",
    "fromAccountId": "cmabcd1234",
    "toAccountId": null,
    "toAccountNumber": "PB-9Z1X2Y",
    "amount": "25.00",
    "balanceAfter": "457.13",
    "memo": "Lunch",
    "createdAt": "2026-01-15T09:31:00.000Z"
  }
}

Cards

GET/cardscards:read

List your cards.

Request
curl https://www.prism-bank.com/api/public/v2/cards \
  -H "Authorization: Bearer pb_live_..."
Response
{
  "success": true,
  "data": {
    "object": "list",
    "data": [
      {
        "object": "card",
        "id": "card_1",
        "type": "CREDIT",
        "status": "ACTIVE",
        "tier": "QUARTZ",
        "creditLimit": "2000.00",
        "outstandingBalance": "134.50",
        "expiresAt": "2030-01-15T00:00:00.000Z",
        "createdAt": "2026-01-15T09:30:00.000Z"
      }
    ],
    "has_more": false
  }
}

Loans

GET/loansloans:read

List your loans.

Request
curl https://www.prism-bank.com/api/public/v2/loans \
  -H "Authorization: Bearer pb_live_..."
Response
{
  "success": true,
  "data": {
    "object": "list",
    "data": [
      {
        "object": "loan",
        "id": "loan_1",
        "status": "ACTIVE",
        "principal": "10000.00",
        "outstandingPrincipal": "8412.30",
        "interestRateBps": 450,
        "termMonths": 36,
        "nextPaymentDueDate": "2026-02-01T00:00:00.000Z",
        "nextPaymentAmount": "312.44",
        "disbursedAt": "2025-06-01T00:00:00.000Z",
        "createdAt": "2025-06-01T00:00:00.000Z"
      }
    ],
    "has_more": false
  }
}

Invoices

GET/invoicesinvoices:read

List invoices for every business your bank profile owns.

Request
curl https://www.prism-bank.com/api/public/v2/invoices \
  -H "Authorization: Bearer pb_live_..."
Response
{
  "success": true,
  "data": {
    "object": "list",
    "data": [
      {
        "object": "invoice",
        "id": "inv_1",
        "number": "INV-0042",
        "status": "SENT",
        "total": "1200.00",
        "dueDate": "2026-02-01T00:00:00.000Z",
        "paidAt": null,
        "createdAt": "2026-01-15T09:30:00.000Z"
      }
    ],
    "has_more": false
  }
}

Gateway payments

POST/gateway/paymentsgateway:writeidempotent

Pay a merchant by their payout account number, from one of your accounts.

Request
curl https://www.prism-bank.com/api/public/v2/gateway/payments \
  -H "Authorization: Bearer pb_live_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"merchantAccountNumber":"PB-4M8K2P","payerBankAccountId":"cmabcd1234","amount":"19.99"}'
Response
{
  "success": true,
  "data": {
    "object": "gateway_payment",
    "id": "gwp_1",
    "status": "SETTLED",
    "amount": "19.99",
    "feeAmount": "0.55",
    "netAmount": "19.44",
    "createdAt": "2026-01-15T09:32:00.000Z"
  }
}

Payment requests

GET/pay/requestsaccounts:read

List payment requests addressed to you, awaiting your action.

Request
curl https://www.prism-bank.com/api/public/v2/pay/requests \
  -H "Authorization: Bearer pb_live_..."
Response
{
  "success": true,
  "data": {
    "object": "list",
    "data": [
      {
        "object": "payment_request",
        "id": "preq_1",
        "status": "PENDING",
        "amount": "45.00",
        "memo": "Split dinner",
        "expiresAt": "2026-01-22T09:30:00.000Z",
        "resolvedAt": null,
        "createdAt": "2026-01-15T09:30:00.000Z"
      }
    ],
    "has_more": false
  }
}
POST/pay/requests/{id}/approvetransfers:writeidempotent

Approve a request addressed to you, funding it from one of your accounts.

Request
curl https://www.prism-bank.com/api/public/v2/pay/requests/preq_1/approve \
  -H "Authorization: Bearer pb_live_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"fromAccountId":"cmabcd1234"}'
Response
{
  "success": true,
  "data": {
    "object": "payment_request",
    "id": "preq_1",
    "status": "APPROVED",
    "amount": "45.00",
    "memo": "Split dinner",
    "expiresAt": "2026-01-22T09:30:00.000Z",
    "resolvedAt": "2026-01-15T09:33:00.000Z",
    "createdAt": "2026-01-15T09:30:00.000Z"
  }
}

Webhooks

Subscribe an HTTPS endpoint to real events instead of polling. Register and manage endpoints from /dashboard/developer/webhooks (signed in) — there's no API for creating them, only the dashboard.

Events

transfer.completed

A transfer between accounts, or to another member, completed.

{
  "object": "event",
  "type": "transfer.completed",
  "createdAt": "2026-01-15T09:30:00.000Z",
  "data": {
    "fromAccountId": "cmabcd1234",
    "toAccountNumber": "PB-9Z1X2Y",
    "amount": "25.00"
  }
}
payment.settled

A gateway payment settled to the merchant (instantly, or after the merchant's hold period).

{
  "object": "event",
  "type": "payment.settled",
  "createdAt": "2026-01-15T09:30:00.000Z",
  "data": {
    "paymentId": "gwp_1",
    "merchantId": "merch_1",
    "amount": "19.99",
    "netAmount": "19.44"
  }
}
request.paid

A payment request you created was approved by the payer.

{
  "object": "event",
  "type": "request.paid",
  "createdAt": "2026-01-15T09:30:00.000Z",
  "data": {
    "requestId": "preq_1",
    "amount": "45.00",
    "payerBankProfileId": "bp_9"
  }
}
loan.approved

A loan application you submitted was approved.

{
  "object": "event",
  "type": "loan.approved",
  "createdAt": "2026-01-15T09:30:00.000Z",
  "data": {
    "applicationId": "app_1",
    "approvedAmount": "10000.00",
    "approvedTermMonths": 36,
    "approvedInterestRateBps": 450
  }
}
card.authorized

One of your cards authorized a purchase.

{
  "object": "event",
  "type": "card.authorized",
  "createdAt": "2026-01-15T09:30:00.000Z",
  "data": {
    "cardId": "card_1",
    "merchantId": "merch_1",
    "amount": "12.50",
    "gatewayPaymentId": "gwp_2"
  }
}
cashback.posted

Earned cashback settled to your account.

{
  "object": "event",
  "type": "cashback.posted",
  "createdAt": "2026-01-15T09:30:00.000Z",
  "data": {
    "cardId": "card_1",
    "amount": "0.25",
    "cardCashbackEntryId": "cbe_1"
  }
}
invoice.paid

An invoice for one of your businesses was paid — by any method.

{
  "object": "event",
  "type": "invoice.paid",
  "createdAt": "2026-01-15T09:30:00.000Z",
  "data": {
    "invoiceId": "inv_1",
    "number": "INV-0042",
    "total": "1200.00",
    "method": "card"
  }
}

Verifying the signature

Every delivery includes a Prism-Signature header: t=<unix timestamp>,v1=<hex hmac>. The signature is an HMAC-SHA256 of ${t}.${rawBody}, keyed with the signing secret shown once when you created the endpoint. Verify it before trusting the payload — anyone can POST to your endpoint claiming to be us.

verify-webhook.ts
import { createHmac, timingSafeEqual } from "crypto";

export function verifyPrismWebhook(rawBody: string, signatureHeader: string, secret: string): boolean {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((p) => p.split("=") as [string, string]),
  );
  const timestamp = parts.t;
  const signature = parts.v1;
  if (!timestamp || !signature) return false;

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  // Constant-time comparison — a plain === leaks timing information.
  const a = Buffer.from(signature, "hex");
  const b = Buffer.from(expected, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}

Retries

A non-2xx response (or no response within 5 seconds) is retried with exponential backoff: 1 minute, 5 minutes, 30 minutes, 2 hours, then 6 hours — 5 attempts total. If those all fail, the delivery is marked FAILED. If an endpoint accumulates 3 consecutive failed deliveries, it's automatically disabled and you're notified — re-enable it from the dashboard once it's fixed.

Test events

Use "Send test event" on the dashboard to deliver a synthetic test event to an endpoint — signed the same way as a real one — so you can verify your receiver before subscribing to anything real.

Versioning

v1 deprecatedv1 keeps working exactly as it always has — nothing about it is changing — but all new development happens on v2. New integrations should start there.

Migrating from v1

Aspectv1v2
Base path/api/public/v1/api/public/v2
EnvelopeBare JSON — the resource directly, or { error: string }{ success, data } or { success: false, error: { type, code, message } }
Status codes200 on success; every failure is 400Precise: 400/401/403/404/422/429/500 by error type
Rate limit headersNoneX-RateLimit-Limit / -Remaining / -Reset on every response
IdempotencyHonored by write endpoints, but no replay of the original responseFull HTTP-level replay; same key + different body = 422
PaginationNone — full lists onlyCursor-based (?limit, ?starting_after) on every list
Money fieldsDecimal stringsDecimal strings, always 2 places
WebhooksNot availableAvailable

Your existing v1 API keys work on v2 too — scopes carry over. The machine-readable spec is at /api/public/v2/openapi.json.