Billing API reference
Buy credits from the API. When a request fails with 402 insufficient_credits:
GET /credit_packs → POST /checkouts → send the user the checkout_url → poll
GET /checkouts/{id} until status is completed → retry the original request with the same
Idempotency-Key. See Buying credits.
- get/credit_packsList credit packs
- post/checkoutsStart a credit purchase
- get/checkouts/{id}Get a checkout
List credit packs
/api/v1/credit_packs- Read-only keys can call this
Lists the credit packs that can be bought, cheapest first. Call it after a
402 insufficient_credits (or when GET /credits shows can_send: false) to show the user
their options before starting a checkout. The lifetime deal (unlimited: true) is left out
for users who already have it. Returns every pack in one page (has_more is always false).
Request sample
curl https://app.signwith.co/api/v1/credit_packs \
-H "Authorization: Bearer $SIGNWITH_API_KEY"const response = await fetch('https://app.signwith.co/api/v1/credit_packs', {
headers: {
Authorization: `Bearer ${process.env.SIGNWITH_API_KEY}`,
},
})
console.log(response.status, await response.json())import os
import requests
response = requests.get(
"https://app.signwith.co/api/v1/credit_packs",
headers={
"Authorization": f"Bearer {os.environ['SIGNWITH_API_KEY']}",
},
)
print(response.status_code, response.json())Response 200
The credit packs on sale. Each item in data is a credit pack object.
{
"object": "list",
"data": [
{
"object": "credit_pack",
"id": 3,
"name": "Basic",
"description": "10 documents",
"credits": 10,
"unlimited": false,
"price": "9.00",
"currency": "USD",
"price_per_credit": "0.90"
},
{
"object": "credit_pack",
"id": 4,
"name": "Pro",
"description": "25 documents",
"credits": 25,
"unlimited": false,
"price": "19.00",
"currency": "USD",
"price_per_credit": "0.76"
},
{
"object": "credit_pack",
"id": 5,
"name": "Business",
"description": "50 documents",
"credits": 50,
"unlimited": false,
"price": "29.00",
"currency": "USD",
"price_per_credit": "0.58"
},
{
"object": "credit_pack",
"id": 9,
"name": "Lifetime deal",
"description": "Unlimited documents, forever",
"credits": null,
"unlimited": true,
"price": "149.00",
"currency": "USD",
"price_per_credit": null
}
],
"has_more": false,
"next_cursor": null
}Errors
| 401 | The API key is missing, invalid or expired, or its account has been archived. (unauthenticated, api_key_expired) |
| 429 | Rate limit exceeded (rate_limited, with Retry-After); more than 20 POST /feedback
messages in an hour (rate_limited, with Retry-After: seconds until the next clock hour); or — for
POST /signature_requests/{id}/remind — signers were reminded less than an hour ago
(remind_too_soon, with Retry-After). (rate_limited, remind_too_soon) |
| 500 | Something went wrong on SignWith's side. It's safe to retry with the same Idempotency-Key. (internal_error) |
Codes listed are the examples the spec gives; see all error codes.
Start a credit purchase
/api/v1/checkouts- Needs a full-access key
- Accepts Idempotency-Key
Starts buying a credit pack and returns a hosted checkout_url for the user to pay on.
Payment happens entirely on the payment provider's page; credits are added once the provider
confirms the payment. Poll GET /checkouts/{id} to find out when
that happens, then retry the request that failed with 402 insufficient_credits.
An optional discount_code applies a discount if it is active, not used up and valid for
the chosen pack (422 invalid_discount_code otherwise); the returned credit_pack then
describes the discounted offer. Requires a full-access key and a billing country on the
user's SignWith profile (422 billing_details_required). Buying the lifetime deal twice
returns 422 already_purchased. If the payment provider can't start the payment the API
returns 502 payment_provider_error.
Each call starts a new checkout, so send an Idempotency-Key to make retries safe.
Headers
Idempotency-KeystringA unique value (e.g. a UUID) that makes retries of this request safe. A successful response is stored for 24 hours and replayed for retries with the same key.
at most 255 characters
Request body
credit_pack_idintegerRequiredID of a pack from
GET /credit_packs.discount_codestringOptional discount code for this pack.
Request sample
curl -X POST https://app.signwith.co/api/v1/checkouts \
-H "Authorization: Bearer $SIGNWITH_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"credit_pack_id": 4
}'const response = await fetch('https://app.signwith.co/api/v1/checkouts', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SIGNWITH_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
credit_pack_id: 4,
}),
})
console.log(response.status, await response.json())import os
import uuid
import requests
response = requests.post(
"https://app.signwith.co/api/v1/checkouts",
headers={
"Authorization": f"Bearer {os.environ['SIGNWITH_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"credit_pack_id": 4,
},
)
print(response.status_code, response.json())Example: With a discount code
curl -X POST https://app.signwith.co/api/v1/checkouts \
-H "Authorization: Bearer $SIGNWITH_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"credit_pack_id": 4,
"discount_code": "LAUNCH20"
}'const response = await fetch('https://app.signwith.co/api/v1/checkouts', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SIGNWITH_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
credit_pack_id: 4,
discount_code: 'LAUNCH20',
}),
})
console.log(response.status, await response.json())import os
import uuid
import requests
response = requests.post(
"https://app.signwith.co/api/v1/checkouts",
headers={
"Authorization": f"Bearer {os.environ['SIGNWITH_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"credit_pack_id": 4,
"discount_code": "LAUNCH20",
},
)
print(response.status_code, response.json())Response 201
The checkout was started. Send the user to checkout_url. Returns a checkout object.
Example response: see the example object below.
Errors
| 400 | The request body is not valid JSON. (invalid_json) |
| 401 | The API key is missing, invalid or expired, or its account has been archived. (unauthenticated, api_key_expired) |
| 403 | The key is read-only, or its user can't access this resource. (read_only_api_key, forbidden) |
| 404 | The resource doesn't exist or belongs to another account. (not_found) |
| 409 | A request with the same Idempotency-Key is still being processed. Retry shortly. (idempotency_key_in_use) |
| 422 | The request is valid JSON but can't be processed. See error.code; the codes each endpoint
can return are described in the endpoint's description and in the error table above. (missing_parameter, invalid_parameter, invalid_request, missing_documents, too_many_documents, document_too_large, invalid_document, invalid_file_type, pdf_encrypted, missing_signers, invalid_signer, already_completed, not_open, nothing_to_remind, invalid_field, template_has_no_fields, template_archived, signer_finished, idempotency_key_reused, invalid_discount_code, already_purchased, billing_details_required) |
| 429 | Rate limit exceeded (rate_limited, with Retry-After); more than 20 POST /feedback
messages in an hour (rate_limited, with Retry-After: seconds until the next clock hour); or — for
POST /signature_requests/{id}/remind — signers were reminded less than an hour ago
(remind_too_soon, with Retry-After). (rate_limited, remind_too_soon) |
| 500 | Something went wrong on SignWith's side. It's safe to retry with the same Idempotency-Key. (internal_error) |
| 502 | The payment provider couldn't start the checkout. Nothing was charged; try again later. (payment_provider_error) |
Codes listed are the examples the spec gives; see all error codes.
Get a checkout
/api/v1/checkouts/{id}- Read-only keys can call this
Returns a checkout's status. Poll it after sending the user the checkout_url:
pending means the user hasn't paid yet (or the payment is still being confirmed),
completed means the credits have been added, and failed means the payment failed or was
canceled — start a new checkout to try again. checkout_url is only returned while the
checkout is pending. Only checkouts started by the key's user can be read.
Path parameters
idstring (uuid)RequiredCheckout ID (the
idreturned byPOST /checkouts).
Request sample
curl https://app.signwith.co/api/v1/checkouts/3f6b2c1a-8d4e-4b7a-9c20-5e1f7a9d3b84 \
-H "Authorization: Bearer $SIGNWITH_API_KEY"const response = await fetch('https://app.signwith.co/api/v1/checkouts/3f6b2c1a-8d4e-4b7a-9c20-5e1f7a9d3b84', {
headers: {
Authorization: `Bearer ${process.env.SIGNWITH_API_KEY}`,
},
})
console.log(response.status, await response.json())import os
import requests
response = requests.get(
"https://app.signwith.co/api/v1/checkouts/3f6b2c1a-8d4e-4b7a-9c20-5e1f7a9d3b84",
headers={
"Authorization": f"Bearer {os.environ['SIGNWITH_API_KEY']}",
},
)
print(response.status_code, response.json())Response 200
The checkout. Returns a checkout object.
{
"object": "checkout",
"id": "3f6b2c1a-8d4e-4b7a-9c20-5e1f7a9d3b84",
"status": "completed",
"credit_pack": {
"object": "credit_pack",
"id": 4,
"name": "Business",
"description": "50 documents",
"credits": 50,
"unlimited": false,
"price": "29.00",
"currency": "USD",
"price_per_credit": "0.58"
},
"amount": "29.00",
"currency": "USD",
"discount_code": null,
"checkout_url": null,
"completed_at": "2026-09-27T11:24:09Z",
"created_at": "2026-09-27T11:20:31Z"
}Example response: see the example object below.
Errors
| 401 | The API key is missing, invalid or expired, or its account has been archived. (unauthenticated, api_key_expired) |
| 404 | The resource doesn't exist or belongs to another account. (not_found) |
| 429 | Rate limit exceeded (rate_limited, with Retry-After); more than 20 POST /feedback
messages in an hour (rate_limited, with Retry-After: seconds until the next clock hour); or — for
POST /signature_requests/{id}/remind — signers were reminded less than an hour ago
(remind_too_soon, with Retry-After). (rate_limited, remind_too_soon) |
| 500 | Something went wrong on SignWith's side. It's safe to retry with the same Idempotency-Key. (internal_error) |
Codes listed are the examples the spec gives; see all error codes.
The credit pack object
A pack of credits that can be bought with POST /checkouts.
objectstringRequiredAlwayscredit_packidintegerRequirednamestringRequireddescriptionstring or nullRequiredcreditsinteger or nullRequiredCredits added when bought.
nullfor the lifetime deal.unlimitedbooleanRequiredtruefor the lifetime deal, which removes the credit limit.pricestringRequiredPrice as a decimal string with two places.
currencystringRequiredAlwaysUSDprice_per_creditstring or nullRequiredpricedivided bycredits, as a decimal string.nullfor the lifetime deal.
The checkout object
A credit purchase started with POST /checkouts.
objectstringRequiredAlwayscheckoutidstring (uuid)RequiredCheckout ID. Use it with
GET /checkouts/{id}.statusstringRequiredpending— waiting for payment or confirmation;completed— paid and credits added;failed— the payment failed or was canceled.One ofpending,completed,failedcredit_packobjectRequiredThe pack being bought (the discounted offer when a
discount_codewas applied).9 child fields
objectstringRequiredAlwayscredit_packidintegerRequirednamestringRequireddescriptionstring or nullRequiredcreditsinteger or nullRequiredCredits added when bought.
nullfor the lifetime deal.unlimitedbooleanRequiredtruefor the lifetime deal, which removes the credit limit.pricestringRequiredPrice as a decimal string with two places.
currencystringRequiredAlwaysUSDprice_per_creditstring or nullRequiredpricedivided bycredits, as a decimal string.nullfor the lifetime deal.
amountstringRequiredAmount charged, as a decimal string.
currencystringRequiredAlwaysUSDdiscount_codestring or nullRequiredcheckout_urlstring or nullRequiredHosted payment page to send the user to. Only present while
statusispending.URLcompleted_atstring (date-time) or nullRequiredWhen the payment was confirmed.
created_atstring (date-time)Required
{
"object": "checkout",
"id": "3f6b2c1a-8d4e-4b7a-9c20-5e1f7a9d3b84",
"status": "pending",
"credit_pack": {
"object": "credit_pack",
"id": 4,
"name": "Business",
"description": "50 documents",
"credits": 50,
"unlimited": false,
"price": "29.00",
"currency": "USD",
"price_per_credit": "0.58"
},
"amount": "29.00",
"currency": "USD",
"discount_code": null,
"checkout_url": "https://checkout.dodopayments.com/buy/pl_2x7Kq9mTbR4vLw8N",
"completed_at": null,
"created_at": "2026-09-27T11:20:31Z"
}Base URL, pagination and idempotency work the same on every endpoint: see making requests. Every error code is on errors.
Start building
The API and MCP server come with every account. 3 free documents a month, then pay per document.