Skip to content

Credits ​

Prepaid letters. An API-created campaign spends them; a campaign the balance cannot cover is refused with 402 insufficient_credits before anything is rendered or charged.

Why prepaid. An integration cannot stop and wait for someone to enter a card, so the money moves first. The balance is also the ceiling: a runaway loop can spend what was bought and no more.

Permissions needed: campaigns:read.

Credits are bought in the web app, not through the API. A key may spend the balance it was given; it can never charge the account's card to grow it.

Retrieve the balance ​

GET/api/v1/credits
requires campaigns:read
bash
curl -s -H "x-api-key: $DRSE_API_KEY" \
  "$DRSE_BASE_URL/api/v1/credits"
ts
const res = await fetch(`${BASE}/api/v1/credits`, {
  headers: { "x-api-key": KEY },
});
const { data } = await res.json();
json
{ "data": { "balance": 4200, "welcomeLetterClaimed": true } }

welcomeLetterClaimed says whether the account has taken the single free letter offered in the web app; there is nothing for an API key to claim.

balance is a count of letters, not money. One letter costs one credit regardless of what it cost to buy.

Check before a large send

A campaign of 5 000 letters against a balance of 4 200 fails as a whole — the balance is checked once, for the entire campaign. Reading the balance first turns that into a decision you make rather than an error you handle.

List recent movements ​

GET/api/v1/credits/ledger
requires campaigns:read

The account's statement: every purchase, every campaign that spent letters, and every refund, newest first. Up to 50 entries.

bash
curl -s -H "x-api-key: $DRSE_API_KEY" \
  "$DRSE_BASE_URL/api/v1/credits/ledger"
ts
const res = await fetch(`${BASE}/api/v1/credits/ledger`, {
  headers: { "x-api-key": KEY },
});
const { data } = await res.json();
json
{
  "data": [
    {
      "delta": -1200,
      "reason": "consumption",
      "campaignId": "550e8400-e29b-41d4-a716-446655440000",
      "note": null,
      "createdAt": "2026-08-13 21:04:11"
    },
    {
      "delta": 5000,
      "reason": "purchase",
      "campaignId": null,
      "note": null,
      "createdAt": "2026-08-12 09:31:02"
    }
  ]
}
FieldMeaning
deltaSigned letters. Negative spends, positive adds.
reasonpurchase, consumption, refund, adjustment or welcome (the one free letter a new account claims in the web app)
campaignIdThe campaign a consumption or refund belongs to; null otherwise
noteSet on operator adjustments only

The entries sum to the balance. That is the point of publishing them: a balance you disagree with can be checked line by line rather than argued about.

Running out ​

A campaign that needs more letters than the balance holds is refused whole — no partial send, nothing rendered, nothing charged:

json
{
  "error": {
    "code": "insufficient_credits",
    "message": "The campaign needs 1200 letters but the balance is 300. Top up and retry."
  }
}

The message carries both numbers so the shortfall needs no arithmetic. Top up in the web app, then retry the identical request — including its Idempotency-Key, which is still safe to reuse: the first attempt created no campaign.