Home/Developers/API Reference
DevelopersAPI Reference

Stablecoin Payment API. One endpoint. Five chains.

REST + JSON over HTTPS. Bearer-token authentication. One POST creates a payment link valid across USDC and USDT on Ethereum, Base, Polygon, Arbitrum, and BNB Chain.

//The modelone create, two patterns01/09

One create endpoint. Two integration patterns.

POST /v1/payments returns a paymentUrl. You can share it directly (payment-link pattern) or redirect / iframe it into your checkout (hosted-checkout pattern). The customer-facing flow is identical either way: only your distribution changes. There's no separate /checkout or /payment-links endpoint to wire up.

//Endpointsone to create · two auxiliary02/09

Integration is a single POST. GET and DELETE are auxiliary, used to retrieve current payment state or cancel an open one.

POST

/v1/payments

Create a payment. Returns a paymentUrl your customer can pay at, valid across the chains and tokens you accept.

Body
Required: amount (fiat string), currency (one of 14 fiat codes).
Returns
Optional: title, externalId, acceptedChains, acceptedTokens, expiresAt (15 min to 30 days, default 1 hour), sellers (for marketplace splits).
Notes
Returns: id, status=ACTIVE, paymentUrl, pymstrFee (1%), merchantFee, expiresAt, createdAt.
GET

/v1/payments/{id}

Retrieve a payment. Returns current state plus on-chain data once paid.

Body
Returns the merchant view: status, chainId, token, exchangeRate, amountToken, txHash, paidAt.
Returns
Use to poll for completion if you choose not to consume webhooks.
DELETE

/v1/payments/{id}

Cancel an ACTIVE payment. The customer-facing paymentUrl stops accepting transactions.

Body
Only valid when status is ACTIVE.
Returns
Returns the updated payment with status=CANCELLED.

The full surface

Beyond the payment lifecycle, the same Bearer key drives webhook configuration, API-key management, analytics, and the transaction ledger.

Payments
POST /v1/payments · GET /v1/payments/:id · GET /v1/payments · DELETE /v1/payments/:idCreate, retrieve, list (cursor-paginated), and cancel payments.
Webhook config
GET / POST / PUT /v1/webhooks · POST /v1/webhooks/regenerate-secret · POST /v1/webhooks/test · GET /v1/webhooks/deliveriesManage the endpoint, rotate the secret, send test events, inspect deliveries.
API keys
GET / POST /v1/auth/api-keys · DELETE /v1/auth/api-keys/:idCreate, list, and revoke pk_ keys. Optional IP whitelist, expiry from 30 days to never.
Analytics
GET /v1/analytics · GET /v1/analytics/exportPayment summary, splits by stablecoin and chain, 30-day CSV export.
Transactions
GET /v1/transactionsLedger records for every settlement, cursor-paginated.
Web3 proxy
POST /v1/web3/:chainIdJSON-RPC pass-through powering the checkout flow (eth_sendUserOperation).
//Create a paymentenforced rails · atomic split03/09

One POST. Full control of the rail.

acceptedChains and acceptedTokens enforce exactly which networks and stablecoins the customer can pay with, so wrong-chain payments are impossible by construction. sellers[] splits the payment across up to 5 wallets in the same on-chain transaction, with the 1% fee deducted proportionally from every recipient. The basic create is on the quickstart; this is the version marketplaces ship.

POST /v1/paymentsserver-side
// Enforced payment with a marketplace split: the customer
// can ONLY pay USDC on Arbitrum or Base, and one on-chain
// transaction pays all three wallets atomically.
const res = await fetch('https://api.pymstr.com/v1/payments', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.PYMSTR_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    amount: '250.00',
    currency: 'USD',
    title: 'Premium Plan',
    externalId: 'sub-456',           // your reference. Unique while active
    acceptedChains: [42161, 8453],   // Arbitrum + Base only
    acceptedTokens: ['USDC'],
    merchantAmount: '180.00',
    sellers: [
      { address: '0x1234...abcd', amount: '60.00' },
      { address: '0x5678...efgh', amount: '10.00' },
    ],
  }),
});

const payment = await res.json();
// payment.paymentUrl -> share it, redirect to it, or iframe it
201 Createdresponse
{
  "id": "01234567-89ab-cdef-0123-456789abcdef",
  "status": "ACTIVE",
  "paymentUrl": "https://pay.pymstr.com/p/01234567-...",
  "pymstrFee": "2.50",
  "merchantFee": "178.20",
  "sellers": [
    { "address": "0x1234...abcd", "amount": "59.40" },
    { "address": "0x5678...efgh", "amount": "9.90" }
  ],
  "expiresAt": "2026-03-01T00:00:00.000Z",
  "createdAt": "2026-02-24T10:00:00.000Z"
}
//Recipe · USDC on every chain4 chains · 1 request04/09

USDC payments across multiple chains. One request.

Native USDC lives on four of the five supported networks: Ethereum (1), Arbitrum One (42161), Base (8453) and Polygon (137). BNB Smart Chain (56) carries USDT only. To accept USDC everywhere it exists, list those four chain IDs in acceptedChains and USDC in acceptedTokens. The hosted checkout only offers those combinations, so a wrong-network payment cannot happen, and the customer pays the gas, so the network they choose only changes their cost (per-network gas and speed are in Chains + tokens below).

After the payment confirms, GET /v1/payments/{id} carries the singular chainId and token the customer actually used, plus txHash and amountToken. Reconcile per network on chainId; the plural acceptedChains stays exactly what you sent.

POST /v1/paymentsserver-side
// Accept USDC on every network that carries it natively:
// Ethereum (1), Arbitrum One (42161), Base (8453), Polygon (137).
// BNB Smart Chain (56) is USDT-only, so it is left out.
const res = await fetch('https://api.pymstr.com/v1/payments', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.PYMSTR_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    amount: '99.00',
    currency: 'USD',
    title: 'Order 8841',
    externalId: 'order-8841',
    acceptedChains: [1, 42161, 8453, 137],
    acceptedTokens: ['USDC'],
  }),
});

const payment = await res.json();
// payment.paymentUrl -> the customer picks the network at checkout
GET /v1/payments/{id}response
{
  "id": "01234567-89ab-cdef-0123-456789abcdef",
  "status": "SUCCESSFUL",
  "amount": "99.00",
  "currency": "USD",
  "title": "Order 8841",
  "externalId": "order-8841",
  "acceptedChains": [1, 42161, 8453, 137],
  "acceptedTokens": ["USDC"],
  "chainId": 137,
  "token": "USDC",
  "exchangeRate": "1.001500",
  "amountToken": "99.148500",
  "pymstrFee": "0.99",
  "merchantFee": "98.01",
  "txHash": "0x...",
  "paymentUrl": "https://pay.pymstr.com/p/01234567-...",
  "paidAt": "2026-08-30T10:05:00.000Z",
  "createdAt": "2026-08-30T10:00:00.000Z"
}
//AuthenticationBearer token · pk_ prefix05/09

One header. Bearer token.

Every API call carries an Authorization header with your secret API key. Keys are created in the PYMSTR dashboard and prefixed with pk_.

Use pk_ keys server-side only. Never ship a key in a browser bundle. See docs.pymstr.com for full authentication details, including session-token usage and security features.

Authorization headerexample
Authorization: Bearer pk_live_abc123...xyz

# Test the connection:
curl https://api.pymstr.com/v1/payments \
  -H "Authorization: Bearer pk_live_..." \
  -X GET
//Payment states6 possible values06/09

Six states. One forward direction.

ACTIVEPayment created, waiting for the customer
PROCESSINGCustomer selected a chain/token and submitted the transaction
SUCCESSFULOn-chain transaction confirmed
FAILEDTransaction reverted or bundler error
EXPIREDPayment reached its expiresAt without completing
CANCELLEDMerchant cancelled the payment via DELETE /v1/payments/{id}
//Safe retriesexternalId dedupe · 409 on duplicate07/09

Retries that can't double-charge.

Pass your own reference (an order ID, a subscription ID) as externalId. It must be unique per merchant while a payment is active or successful, so a retried create call can never mint a second live payment: the API answers 409 Conflict instead. Once a payment fails, expires, or is cancelled, the externalId becomes reusable.

List endpoints paginate with a cursor: pass the last item's id as cursor and read nextCursor + hasMore from the response (limit defaults to 20, max 100). Validation failures return a structured errors array, so a 400 tells you exactly which field to fix.

Error shapesreference
// Duplicate externalId while a payment is live:
HTTP/1.1 409 Conflict
{
  "statusCode": 409,
  "message": "externalId already in use",
  "error": "Conflict"
}

// Validation failure shape:
HTTP/1.1 400 Bad Request
{
  "statusCode": 400,
  "message": "Validation failed",
  "error": "Bad Request",
  "errors": [
    { "path": ["amount"], "message": "Invalid amount format" }
  ]
}
//Limits + coveragerate · chains · currencies08/09

Rate limits

Every response includes rate-limit headers. When you exceed the limit, the API returns 429 Too Many Requests.

  • X-RateLimit-LimitMaximum requests allowed in the window
  • X-RateLimit-RemainingRequests remaining in the current window
  • X-RateLimit-ResetUnix timestamp when the window resets

Current limits per endpoint are documented at docs.pymstr.com.

Chains + tokens

  • Ethereum id=1USDC, USDT
  • Arbitrum One id=42161USDC, USDT
  • Base id=8453USDC
  • Polygon id=137USDC, USDT
  • BNB Smart Chain id=56USDT

14 fiat currencies for the amount field: USD · EUR · GBP · AED · ARS · BRL · IDR · ILS · JPY · MXN · NGN · PHP · THB · VND.

//API Reference questions6 answers09/09
A REST API that lets your backend create and track payments settled in stablecoins (USDC and USDT) instead of card rails. With PYMSTR, one POST to /v1/payments returns a hosted paymentUrl that accepts USDC or USDT across Ethereum, Base, Polygon, Arbitrum, and BNB Chain. Settlement is on-chain, direct to your wallet, with a 1% flat fee and no chargebacks. You integrate two things: the create call and the payment.completed webhook.
One POST to /v1/payments with an amount and one of 14 fiat currencies. The response includes a paymentUrl you can share, redirect to, or embed. Optional fields give you control: acceptedChains and acceptedTokens enforce exactly which networks and stablecoins the customer can pay with, expiresAt bounds the payment window (15 minutes to 30 days), and sellers[] splits the payment across up to 5 wallets in a single on-chain transaction.
Use the externalId field. It is your own reference (an order ID, a subscription ID) and must be unique per merchant while a payment is active or successful, so retrying a create call with the same externalId cannot produce a duplicate live payment: the API returns 409 Conflict instead. After a payment fails, expires, or is cancelled, the externalId becomes reusable. There is no separate idempotency-key header; externalId is the deduplication mechanism.
USDC and USDT across 5 chains: Ethereum (1), Polygon (137), Arbitrum (42161), Base (8453), and BNB Chain (56). Base is USDC-only and BNB Chain is USDT-only; the other three support both. Pass chain IDs in acceptedChains and token symbols in acceptedTokens to restrict a payment, or omit both to accept everything.
Bearer authentication with a secret key prefixed pk_, created in the dashboard. Keys can be restricted to specific IP addresses (up to 20) and set to expire after 30, 90, or 180 days, 1 year, or never. You can hold up to 10 active keys, which makes zero-downtime rotation straightforward: create the new key, deploy it, then revoke the old one. Keys are server-side only; never ship one in a browser bundle.
Pass acceptedTokens: ["USDC"] and acceptedChains: [1, 42161, 8453, 137] on POST /v1/payments. Those are the four networks that carry native USDC (Ethereum, Arbitrum One, Base, Polygon); BNB Smart Chain is USDT-only. The customer picks the network at checkout and pays the gas. Read the payment back with GET /v1/payments/{id}: chainId and token tell you which network and stablecoin they actually used, and txHash is the on-chain proof.

Build with the stablecoin rail.