Auth: the user-facing routes (POST /users/{user_id}/contributions/self, GET /users/{user_id}/orders/{id}) use bearer JWT with self-scope — jwt.sub == user_id on the path. The admin cross-user list (GET /admin/orders) requires an admin JWT.

Overview

An order is a single funding event crediting a user’s PBA-backed (HSA) account. The orders table is universal across funding flavours; a source URN column on every row identifies the funding leg. Today only the self contribution flow is exposed through the public API. Sponsor contributions (PBA-to-PBA transfers from a sponsor’s account) share the same table shape but settle through a separate, internal flow.

Funding source (source)

Every order carries a source field on the response, URN-encoded: Validation runs at every entry point — JSON deserialization and DB row reads. A malformed URN is a 400 with the parse error.

Order lifecycle (self contribution)

  1. CreatePOST /users/{user_id}/contributions/self takes { amount, currency } (amount in major units, currency an ISO-4217 code controlling the major→minor conversion), opens a Juspay session, and inserts an orders row with source = "self_contribution", internal status = INITIATED, and a null external_status. The Juspay /session response is returned verbatim as payload (the frontend uses payload.sdk_payload to render the payment page).
  2. PollGET /users/{user_id}/orders/{id} calls Juspay /orders/{id} on every invocation, merges the latest external_status (CHARGED / PENDING_VBV / FAILED / verbatim Other(...) for unknown values) and payment-method fields into the row, and returns the refreshed snapshot. Frontend controls polling cadence — there is no server-side short-circuit.
  3. Settle on CHARGED — the first poll observing CHARGED confirms the PBA hold and credits the user’s HSA, advancing the internal status to SUCCESS and wallet_txn_data.status to SUCCESS. Once wallet_txn_data.status is terminal, subsequent polls do nothing. PBA’s idempotency_key = order.id guards against double-deposits even if the gate is bypassed.
Sponsor-funded orders do not transit GET /users/{user_id}/orders/{id} — they settle synchronously at create time through the sponsor flow and are read via their own surface. GET /admin/orders is a cross-user, paginated admin list of funding orders (for the dashboard Orders tab). It returns lean rows straight off the orders table (no Juspay poll) and filters by user_id, source_kind (self_contribution / sponsor), statuses (comma-separated OrderStatus), and a created_at time range.

Response shape

The SelfContribution create response carries payload (the Juspay SDK blob) but not status / wallet_txn_data. The GetOrderStatus poll and GET /admin/orders rows carry status and wallet_txn_data but not payload. All three share the remaining fields.

Error codes