---
title: "Start a credit checkout"
method: POST
path: "/credits/checkout"
tags: ["Credits"]
---

# Start a credit checkout

`POST /credits/checkout`

## Request

- Base URL: `https://inkseal.net/api/v1`
- URL: `https://inkseal.net/api/v1/credits/checkout`
- Auth: HTTP bearer

Opens a Stripe Checkout session for one credit pack and returns its URL.

This is what to do about `insufficient_credits`. It buys nothing by itself: the URL is a payment page a human opens, and the balance moves only when Stripe confirms the payment, which arrives as an ordinary `purchase` row in `listCreditTransactions`. So the workflow is to create the checkout, hand the URL to the person the agent is working for, and stop. Do not poll `getCreditBalance` in a loop waiting for a card to be typed.

`billing:manage` is required on top of `account:read`, and only a workspace owner can grant it, so a credential that can reach this endpoint was minted by the one person entitled to spend the workspace's money.

`successUrl` and `cancelUrl` are where Stripe returns the payer afterwards. Both must be absolute `https` URLs with no embedded credentials; anything else answers `400` `validation_error`. Omit them to use the dashboard credits page.

## Headers

- `Idempotency-Key` string

## Request body

- object
  - `packId` 'pack_5' | 'pack_25' | 'pack_50' | 'pack_100' | 'pack_500', required: Which pack to sell. The sizes and prices are the ones published at https://inkseal.net/pricing.
  - `successUrl` string, uri: Absolute `https` URL Stripe returns the payer to after a completed payment. Defaults to the dashboard credits page.
  - `cancelUrl` string, uri: Absolute `https` URL Stripe returns the payer to if they abandon the checkout. Defaults to the dashboard credits page.

## Response `201`

The checkout session. No credits have been added yet.

- object
  - `data` CreditCheckout, required: A Stripe Checkout session a human completes to buy credits. Nothing is charged and no credit is added until they do.
    - `checkoutUrl` string, uri, required: The Stripe-hosted payment page. Hand this to the person the agent is working for; it needs a card, so an agent cannot complete it.
    - `expiresAt` string, date-time, required: When the page stops working. Stripe sets this; after it passes, create a new checkout.
    - `packId` 'pack_5' | 'pack_25' | 'pack_50' | 'pack_100' | 'pack_500', required: The pack this session sells.
    - `credits` integer, required: Credits the workspace receives once the payment settles. A send spends one credit for the envelope plus one per signer.
    - `amountCents` integer, required: Price in integer cents.
    - `currency` 'usd', required: Always `usd`. Credits are priced in one currency.

## Other responses

- `400`: The request is malformed or the resource is in the wrong state for a synchronous check. Codes: `validation_error`, `invalid_body`, `invalid_parameter`, `invalid_idempotency_key`, `invalid_event_id`, `invalid_file`, `invalid_url`, `invalid_status`, `key_mismatch`, `no_signers`, `not_completed`, `envelope_exists`, `document_not_ready`, `upload_failed`.
- `401`: The credential is missing, malformed, unknown, expired, revoked, or past its rotation grace window. Code: `unauthorized`. Every authentication failure is deliberately indistinguishable.
- `403`: The credential is valid but may not do this. Codes: `insufficient_scope` (the route's scopes are not all held; `details` names `requiredScopes` and `missingScopes`), `forbidden` (the capability check on the resolved resource failed), `scope_escalation` (a grant wider than the caller's own was requested).
- `409`: The resource's state forbids this transition, or another request owns the work. Codes: `invalid_state`, `invalid_status`, `credential_state`, `limit_exceeded`, `envelope_missing`, `not_retryable`, `owner_unavailable`, `workspace_unavailable`, `detection_not_completed`, `document_in_use`, `version_conflict`, `document_processing`, `document_processing_failed`, `normalized_pdf_unavailable`, `pages_not_rendered`, `document_not_ready`, `idempotency_in_progress` (which also carries `Retry-After`), `payment_replayed` (a USDC payment signature presented twice), `payment_pending` (a USDC settlement whose outcome is not yet known).
- `422`: The request parsed but its content is not acceptable. Codes: `validation_error` (envelope configuration, with an `issues` array in `details`), `invalid_envelope`, `envelope_not_ready`, `document_page_limit`, `idempotency_key_reuse`.
- `429`: A limit was reached. Codes: `rate_limited` (the credential's per-bucket HTTP limit, or the per-workspace detection throttle), `workspace_limit_exceeded` (a workspace content or send limit; `details.limit` names it), `daily_budget_exceeded` (the workspace's daily detection budget). The four rate-limit headers are sent on this response and on no successful one, so a client cannot read its remaining budget without being refused. Retrying after `Retry-After` is always safe: a rate-limit refusal happens before the handler runs.
- `503`: A dependency the request needs is unreachable and nothing was changed. Codes: `queue_unavailable` (a background job could not be enqueued; retry the call), `billing_unavailable` (Stripe would not open a checkout session; nothing was charged, retry the call), `service_unavailable` (the rate-limit store is down, so the request was refused before running).

---

[API](https://skmtc.dev/inkseal/apis/inkseal-api.md) · [All operations](https://skmtc.dev/inkseal/apis/inkseal-api/llms.txt) · [OpenAPI document](https://skmtc.dev/inkseal/apis/inkseal-api/revisions/c440c981d578?raw)
