Skip to content
Developers

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 model

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.

Endpoints

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 (0.5%), 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 payment

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 0.5% 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": "1.25",
  "merchantFee": "179.10",
  "sellers": [
    { "address": "0x1234...abcd", "amount": "59.70" },
    { "address": "0x5678...efgh", "amount": "9.95" }
  ],
  "expiresAt": "2026-03-01T00:00:00.000Z",
  "createdAt": "2026-02-24T10:00:00.000Z"
}
Recipe · USDC on every chain

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.49",
  "merchantFee": "98.51",
  "txHash": "0x...",
  "paymentUrl": "https://pay.pymstr.com/p/01234567-...",
  "paidAt": "2026-08-30T10:05:00.000Z",
  "createdAt": "2026-08-30T10:00:00.000Z"
}
Authentication

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 states

Six states. One forward direction.

ACTIVE
Payment created, waiting for the customer
PROCESSING
Customer selected a chain/token and submitted the transaction
SUCCESSFUL
On-chain transaction confirmed
FAILED
Transaction reverted or bundler error
EXPIRED
Payment reached its expiresAt without completing
CANCELLED
Merchant cancelled the payment via DELETE /v1/payments/{id}
Safe retries

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 + coverage

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

  • Ethereumid=1USDC, USDT
  • Arbitrum Oneid=42161USDC, USDT
  • Baseid=8453USDC
  • Polygonid=137USDC, USDT
  • BNB Smart Chainid=56USDT

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

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 0.5% flat fee and no chargebacks. You integrate two things: the create call and the payment.completed webhook.

Build with the stablecoin rail.