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 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.
{
"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_xxxxxxxxxxxxxxxxxxxxxxxxCreating 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.
| Scope | Grants |
|---|---|
| accounts:read | List and view your bank accounts. |
| transactions:read | View an account's transaction history. |
| transfers:write | Move money between your own accounts, or to another member. Also approves payment requests. |
| gateway:write | Pay a merchant through the gateway. |
| cards:read | List your cards. |
| loans:read | List your loans. |
| invoices:read | List invoices for businesses you own. |
| statements:read | Reserved for future statement endpoints. |
| webhooks:manage | Reserved — 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": true,
"data": "..."
}{
"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.
| Type | Status | When |
|---|---|---|
| authentication_error | 401 | Missing, malformed, invalid, or revoked API key. |
| permission_error | 403 | The key's scopes don't include what this endpoint needs, or the resource exists but isn't yours. |
| invalid_request_error | 400 | Malformed JSON, a validation failure, or an unsupported HTTP method. |
| not_found_error | 404 | The resource doesn't exist at all. |
| business_rule_error | 422 | Understood, but rejected — insufficient funds, an idempotency key reused with a different request, etc. |
| rate_limit_error | 429 | Too many requests for this key in the current window. |
| api_error | 500 | Something 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: 1768470960X-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 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.
Pagination
Every list endpoint is cursor-paginated: ?limit (default 25, max 100) and ?starting_after=<id>.
{
"object": "list",
"data": [
{
"object": "transaction",
"id": "txn_1"
},
{
"object": "transaction",
"id": "txn_2"
}
],
"has_more": true
}Walk every page like this:
let cursor;
const rows = [];
do {
const url = new URL("https://www.prism-bank.com/api/public/v2/accounts");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("starting_after", cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
const { data } = await res.json();
rows.push(...data.data);
cursor = data.has_more ? data.data.at(-1).id : undefined;
} while (cursor);Endpoint reference
Every v2 endpoint. All reads are paginated where they return a list — see Pagination.
Accounts
/accountsaccounts:readList your bank accounts.
curl https://www.prism-bank.com/api/public/v2/accounts \
-H "Authorization: Bearer pb_live_..."{
"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
}
}/accounts/{id}accounts:readGet one account. 404 if it doesn't exist, 403 if it exists but isn't yours.
curl https://www.prism-bank.com/api/public/v2/accounts/cmabcd1234 \
-H "Authorization: Bearer pb_live_..."{
"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"
}
}/accounts/{id}/transactionstransactions:readList an account's ledger history, most recent first.
curl "https://www.prism-bank.com/api/public/v2/accounts/cmabcd1234/transactions?limit=25" \
-H "Authorization: Bearer pb_live_..."{
"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
/transferstransfers:writeidempotentMove money — between two of your own accounts (toAccountId), or to another member (toAccountNumber). Provide exactly one.
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"}'{
"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
/cardscards:readList your cards.
curl https://www.prism-bank.com/api/public/v2/cards \
-H "Authorization: Bearer pb_live_..."{
"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
/loansloans:readList your loans.
curl https://www.prism-bank.com/api/public/v2/loans \
-H "Authorization: Bearer pb_live_..."{
"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
/invoicesinvoices:readList invoices for every business your bank profile owns.
curl https://www.prism-bank.com/api/public/v2/invoices \
-H "Authorization: Bearer pb_live_..."{
"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
/gateway/paymentsgateway:writeidempotentPay a merchant by their payout account number, from one of your accounts.
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"}'{
"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
/pay/requestsaccounts:readList payment requests addressed to you, awaiting your action.
curl https://www.prism-bank.com/api/public/v2/pay/requests \
-H "Authorization: Bearer pb_live_..."{
"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
}
}/pay/requests/{id}/approvetransfers:writeidempotentApprove a request addressed to you, funding it from one of your accounts.
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"}'{
"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.completedA 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.settledA 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.paidA 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.approvedA 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.authorizedOne 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.postedEarned 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.paidAn 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.
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
| Aspect | v1 | v2 |
|---|---|---|
| Base path | /api/public/v1 | /api/public/v2 |
| Envelope | Bare JSON — the resource directly, or { error: string } | { success, data } or { success: false, error: { type, code, message } } |
| Status codes | 200 on success; every failure is 400 | Precise: 400/401/403/404/422/429/500 by error type |
| Rate limit headers | None | X-RateLimit-Limit / -Remaining / -Reset on every response |
| Idempotency | Honored by write endpoints, but no replay of the original response | Full HTTP-level replay; same key + different body = 422 |
| Pagination | None — full lists only | Cursor-based (?limit, ?starting_after) on every list |
| Money fields | Decimal strings | Decimal strings, always 2 places |
| Webhooks | Not available | Available |
Your existing v1 API keys work on v2 too — scopes carry over. The machine-readable spec is at /api/public/v2/openapi.json.