# E-signature API pricing and credits

> The API has no plan or monthly fee of its own. It uses the same pay-per-document credits as the SignWith dashboard, and your code can check the balance and buy more.

Source: https://signwith.co/docs/api/credits · Updated 2026-10-02

## When a credit is used

**One credit covers one signed document**, however many people sign it. Credits are used when a document is signed, not when it's sent.

In the API, a document is a [template](https://signwith.co/docs/api/templates):

- A template uses one credit the first time it's signed.
- Sending the same template again, to anyone, doesn't use another credit.
- Lifetime plans never run out: their balance shows `unlimited: true`.
- The balance may go slightly below zero, down to `overdraft_limit`, before sending is blocked.

Every account gets 3 free documents a month, and credit packs are one-time purchases with no subscription. Current packs and prices are on the [pricing page](https://signwith.co/pricing).

## Check the balance

[`GET /credits`](https://signwith.co/docs/api/account#get-the-credit-balance) returns the balance and `can_send`, which says whether a request that needs a new credit can go out right now. Check it before sending in bulk, so you can ask for a top-up instead of hitting an error halfway.

cURL:

```bash
curl https://app.signwith.co/api/v1/credits \
  -H "Authorization: Bearer $SIGNWITH_API_KEY"
```

Node:

```javascript
const response = await fetch('https://app.signwith.co/api/v1/credits', {
  headers: {
    Authorization: `Bearer ${process.env.SIGNWITH_API_KEY}`,
  },
})

console.log(response.status, await response.json())
```

Python:

```python
import os

import requests

response = requests.get(
    "https://app.signwith.co/api/v1/credits",
    headers={
        "Authorization": f"Bearer {os.environ['SIGNWITH_API_KEY']}",
    },
)

print(response.status_code, response.json())
```

## When credits run out

When the account can't cover a new document, [`POST /signature_requests`](https://signwith.co/docs/api/signature-requests#send-a-signature-request) returns:

```json
{
  "error": {
    "code": "insufficient_credits",
    "message": "Not enough credits to send this document. List credit packs with GET /api/v1/credit_packs and buy one with POST /api/v1/checkouts."
  }
}
```

The status is `402`. Nothing was sent. Show the user the `purchase_url` from [`GET /credits`](https://signwith.co/docs/api/account#get-the-credit-balance) to buy credits in SignWith, or buy them from the API as below.

## Buy credits from the API

An API client or AI agent can top up without leaving your product. Payment always happens on a hosted page: the API never handles card details.

1. List the packs on sale with [`GET /credit_packs`](https://signwith.co/docs/api/billing#list-credit-packs) and let the user choose one.
2. Start a checkout with [`POST /checkouts`](https://signwith.co/docs/api/billing#start-a-credit-purchase), passing `credit_pack_id` and, optionally, a `discount_code`. Send an `Idempotency-Key` so a retry doesn't start a second payment.
3. Send the user to the returned `checkout_url` to pay.
4. Poll [`GET /checkouts/{id}`](https://signwith.co/docs/api/billing#get-a-checkout) every few seconds, then less often, until `status` is `completed` (credits added) or `failed`.
5. Retry the request that failed. If you sent it with an `Idempotency-Key`, reuse the same key and body: failed responses aren't stored, so the retry runs normally.

cURL:

```bash
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
}'
```

Node:

```javascript
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())
```

Python:

```python
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())
```

Use the pack IDs that [`GET /credit_packs`](https://signwith.co/docs/api/billing#list-credit-packs) returns, rather than hard-coding them.

Checkouts need a full-access key and a billing country on the user's SignWith profile; without one the API returns `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` and nothing is charged.

> **AI assistants in ChatGPT:** The MCP server doesn't offer the purchase tools inside ChatGPT. There, the assistant tells the user to add credits in SignWith instead. See [ChatGPT](https://signwith.co/docs/mcp/chatgpt).
