Sandbox

Go to production
PackflipPartner

Errors, idempotency, and retries

Error codes, safe retries, lists, and versioning.

Error shape

Every error response has the same body, so you can branch on error.code:

{ "error": { "code": "insufficient_balance", "message": "Insufficient spendable balance." } }
CodeStatusWhat to do
invalid_request400Fix the request. message names the field. Do not retry unchanged.
unauthorized401Send a valid key as Authorization: Bearer pk_…, for the right environment.
forbidden403The key cannot perform this action.
not_found404The resource does not exist in this account and environment.
conflict409The current state does not allow the action, for example a sealed or already used card. Fetch the resource and decide.
idempotency_conflict409The Idempotency-Key was already used with different data. Use a new key for a new request.
insufficient_balance409Top up, then retry with the same key.
out_of_stock409The vending machine cannot supply the quantity.
service_unavailable503A dependency is not ready. Retry later with backoff.

Unrecognized fields in a request body are rejected with 400, so typos surface immediately. Responses, on the other hand, may gain new fields at any time: ignore fields you do not know.

Idempotency

POST /api/v2/operations requires an Idempotency-Key header of 1 to 255 characters.

  • The first request with a key creates the operation and returns 201.
  • Repeating the same request with the same key returns the original operation with 200 and "replayed": true. Nothing is charged or credited again.
  • Reusing the key with different data returns 409 idempotency_conflict.
  • Keys are scoped to your account and environment and do not expire.

Derive the key from your own record, such as order-{yourOrderId}, and store it before calling the API. If a request times out or fails with a network error, retry with the same key: you will get the operation that was created, or create it now.

Other writes are safe to retry for different reasons:

  • POST /api/v2/customers with an externalUserId returns 409 conflict if the customer exists. Fetch it with GET /api/v2/customers/by-external-id/{externalUserId}.
  • Reveal, refresh, and webhook changes can be repeated without side effects beyond the first.

Retries

Retry network errors, 5xx, and 503 with exponential backoff and jitter, for example 1, 2, 4, 8 seconds, up to a minute. Do not retry 4xx responses other than 409 insufficient_balance after a top-up.

If you receive 429 Too Many Requests, wait for the Retry-After header before retrying. Keep concurrency per account modest; Packflip may apply rate limits to protect the service.

Lists

List endpoints return the newest items first and accept limit:

EndpointDefaultMaximum
GET /api/v2/customers50200
GET /api/v2/customers/{id}/cards50200
GET /api/v2/ledger-entries50200
GET /api/v2/funding-transactions50100
GET /api/v2/funding-intents50100

Keep your own record of operations and customers rather than paging through everything.

Money and time

  • Amounts are decimal strings with six places, such as "12.500000". Use a decimal library; never parse them as floats.
  • Timestamps are ISO 8601 in UTC.
  • IDs are opaque strings with a type prefix: par_, cus_, card_, op_, oco_, ocb_, shp_, wh_, evt_, ftx_. Do not parse them.

Versioning

The API version is a date, currently 2026-09-12, shown in the OpenAPI document and in each webhook's apiVersion. Additive changes can ship at any time. Breaking changes are announced at least 30 days in advance.