Open the console

API

Wallets

Your users' money, and your own. The first seven are the ledger you keep for your customers; the last is your tenant's own balance.

Everything is micro-USD

A wallet's balance, a credit, an adjustment and every statement row are whole micro-USD: 5000000 is five dollars. Divide by 1,000,000 to render. The field is called micros rather than amount on purpose — your tenant's own balance is reported in TOKENS on this same API, and two keys that cannot be confused is cheaper than one whose unit a reader has to know.

A wallet is keyed on (project, your user id)

So ada in one project is a different person with a different balance from ada in another, and a credential issued for one project cannot touch another's wallets. The id is whatever you already call this person — an email, a UUID, your own primary key. It only has to be unique within one project, and it may not be * or contain : or /.

The SDK splits reading from writing, and that is load-bearing

elan.wallets reads — list, get, statement — and elan.credit writes. The read object issues no non-GET at all and a test asserts it, so a read-only credential can be handed wallets and a future write cannot quietly land on it. Reading your own figures is elan.usage.

Creating is idempotent; crediting is not

One wallet per user is enforced, so a repeat create is not a second balance — it answers 200 instead of 201 and is free. A repeat CREDIT issues the credit twice, and ref is the only thing that will tell you so afterwards.

The samples use $API and $TOKEN, which the API overview sets. What your agents consumed, and what the platform charged you, is on the usage page.

Create a wallet

POST/v1/projects/{project}/walletsdeploy-service

Gives one of your users a balance. Safe to call on every sign-in.

201 the first time, 200 afterwards

Both are success and both return the wallet. A backend that calls this whenever somebody signs in is the expected pattern rather than something to code around, and the repeat is not billed.

USD is the only currency accepted

Every price on this platform is quoted in US cents and nothing converts between currencies, so a wallet labeled anything else would be charged incorrectly. The field exists so conversion can arrive later without a migration; until then you can ignore it.

Request
curl -X POST "$API/v1/projects/agents/wallets" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"external_user_id": "ada", "display_label": "Ada Lovelace"}'

Body

FieldTypeDescription
external_user_idstringrequiredWhatever you already call this person. Unique within the project.
display_labelstringoptionalA human-readable name, for your own console and statements.
currencystringoptionalISO 4217. Defaults to USD, which is the only accepted value.

Responses

StatusMeaning
201Created.
200It already existed. Not an error, and not billed.
400The id or the currency was refused — an empty id, *, one containing : or /, or a currency other than USD. The body names which.
403This credential cannot write — it needs Can write — or it is scoped to another project.
404No such project in your tenant.
502The ledger is unreachable. Nothing was written; retry.

Response body

{
  "external_user_id": "ada",
  "project_slug": "agents",
  "display_label": "Ada Lovelace",
  "status": "active",
  "currency": "USD",
  "balance_micros": 0,
  "held_micros": 0,
  "available_micros": 0
}

List your users

GET/v1/projects/{project}/walletsdeploy-service

Every wallet in the project and what each one has left.

The total is in the body, not a header

{wallets, total}, so a paged table can show both its rows and how many there are. A header would be unreadable to any client that returns parsed JSON rather than the response object.

Request
curl "$API/v1/projects/agents/wallets?limit=50&offset=0" \
  -H "Authorization: Bearer $TOKEN"

Parameters

FieldTypeDescription
limitqueryoptional1–200, default 50. Anything larger is clamped rather than refused.
offsetqueryoptionalDefault 0.

Responses

StatusMeaning
200The page, and the total.
403A credential scoped to another project.
404No such project in your tenant.
502The ledger is unreachable.

Response body

{
  "wallets": [
    {
      "external_user_id": "ada",
      "display_label": "Ada Lovelace",
      "status": "active",
      "balance_micros": 4992800,
      "held_micros": 0,
      "available_micros": 4992800
    }
  ],
  "total": 1
}

Read one balance

GET/v1/projects/{project}/wallets/{external_user_id}deploy-service

What you render in your own interface when somebody asks what they have left.

Render available, not balance

held_micros is reserved against a run that has not settled yet, so it is neither spent nor spendable. available_micros is balance − held and is the only one of the three that answers "can they start another run". A balance shown mid-conversation looks wrong otherwise.

Request
curl "$API/v1/projects/agents/wallets/ada" \
  -H "Authorization: Bearer $TOKEN"

Responses

StatusMeaning
200The wallet.
403A credential scoped to another project.
404No such wallet, or no such project in your tenant. An id no wallet could have — *, or one with a colon — is simply not found.
502The ledger is unreachable.

Response body

{
  "external_user_id": "ada",
  "project_slug": "agents",
  "status": "active",
  "currency": "USD",
  "balance_micros": 4992800,
  "held_micros": 12000,
  "available_micros": 4980800
}

Add credit

POST/v1/projects/{project}/wallets/{external_user_id}/creditsdeploy-service

Promotional, prepaid, whatever you are doing. One direction only.

This is not idempotent, and nothing will stop a double credit

Calling it twice issues the credit twice. ref is your own reference — an order id, a promotion name — and it is the only thing that will tell you afterwards which credit was which, so send one even though it is optional.

Request
# $5.00 of promotional credit.
curl -X POST "$API/v1/projects/agents/wallets/ada/credits" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"micros": 5000000, "ref": "promo:launch"}'

Body

FieldTypeDescription
microsintegerrequiredMicro-USD to add, greater than zero; 5000000 is five dollars. A fractional number is truncated to a whole one, not refused.
refstringoptionalYour own reference, carried onto the statement row.

Responses

StatusMeaning
201Credited. {transaction_id, micros}.
400micros was not a number, or the wallet is closed.
403This credential cannot write, or it is scoped to another project.
404No such wallet. Create it first.
502A credit of zero or less, which the ledger refuses and this route does not translate — use an adjustment to take credit away. Or the ledger is unreachable: retry, but read the statement first, because a timeout is not proof it did not land.

Response body

201 {"transaction_id": "txn_3f9c…", "micros": 5000000}

Correct a balance

POST/v1/projects/{project}/wallets/{external_user_id}/adjustmentsdeploy-service

A correction in either direction, which a credit cannot be. Negative claws back a credit applied in error; positive refunds a disputed charge.

The original transaction stays

The books are corrected by a second entry rather than by editing the first, so there is always a row saying where a customer's money went. That is also what makes a dispute answerable six months later.

Request
# Claw back $2.50 credited in error.
curl -X POST "$API/v1/projects/agents/wallets/ada/adjustments" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"micros": -2500000, "ref": "applied in error"}'

Body

FieldTypeDescription
microsintegerrequiredMicro-USD, signed. Negative takes credit away. Zero is refused. Nothing stops an adjustment taking a balance below zero, or below what is held.
refstringoptionalWhy. Carried onto the statement row — write something a colleague will understand.

Responses

StatusMeaning
201Adjusted. {transaction_id, micros}.
400Not a number, zero, or the wallet is closed.
403This credential cannot write, or it is scoped to another project.
404No such wallet.
502The ledger is unreachable.

Freeze, unfreeze or close

POST/v1/projects/{project}/wallets/{external_user_id}/statusdeploy-service

Stop a wallet spending without touching its money, or end it.

Freezing keeps the balance; closing needs a zero one

A frozen wallet refuses spending and keeps its money and its history, and the agent reports it as a suspended account rather than an empty one — which matters, because telling somebody you suspended to top up sends them to do the one thing that cannot help. Closing is refused while credit remains, so refund or write off what is left first.

Setting the status it already has is free

Not an error either. So a reconciliation job that asserts the intended status on every wallet costs nothing for the ones already correct.

Request
curl -X POST "$API/v1/projects/agents/wallets/ada/status" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status": "frozen"}'

Body

FieldTypeDescription
statusstringrequiredOne of active, frozen, closed. active is how you unfreeze.

Responses

StatusMeaning
200The wallet, with its new status.
400Unknown status, or closing a wallet that still holds credit. The body names the known values.
403This credential cannot write, or it is scoped to another project.
404No such wallet.
502The ledger is unreachable.

Read a statement

GET/v1/projects/{project}/wallets/{external_user_id}/statementdeploy-service

One signed row per movement, newest first — credits, adjustments and every run that settled against this wallet.

A run's row carries its invocation id in ref

Which is what makes a surprising charge explainable rather than merely visible: the same id reaches the run's own record and its event history.

Three kinds, and the sign is the direction

grant is a credit, adjustment is a correction either way, and settle is a run's charge. amount_micros is signed from the wallet's side: positive put money in, negative took it out.

Request
curl "$API/v1/projects/agents/wallets/ada/statement?limit=50" \
  -H "Authorization: Bearer $TOKEN"

Parameters

FieldTypeDescription
limitqueryoptional1–200, default 50.
offsetqueryoptionalDefault 0.

Responses

StatusMeaning
200The page, and the total.
403A credential scoped to another project.
404No such wallet.
502The ledger is unreachable.

Response body

{
  "entries": [
    {
      "transaction_id": "txn_8a21…",
      "kind": "settle",
      "ref": "f27bfbb4a1c34e…",
      "amount_micros": -7200,
      "created_at": "2026-10-07T12:05:00+00:00"
    },
    {
      "transaction_id": "txn_3f9c…",
      "kind": "grant",
      "ref": "promo:launch",
      "amount_micros": 5000000,
      "created_at": "2026-10-07T12:00:00+00:00"
    }
  ],
  "total": 2
}

Your own balance

GET/v1/balancedeploy-service

This tenant's prepaid balance and what is currently held against it. Not a wallet — this is the account a run with no end user spends.

Denominated in TOKENS, not money

Your tenant account is quota; a wallet is money. This is the one place on this page where a figure is not micro-USD, and it is why the wallet routes name their field micros.

Request
curl "$API/v1/balance" -H "Authorization: Bearer $TOKEN"

Responses

StatusMeaning
200The account. Zeroes, not a 404, before the first run has created it.
501This deployment has no ledger configured.
502The ledger is unreachable.

Response body

{
  "account_id": "acme",
  "tenant_id": "acme",
  "balance_units": 5000000,
  "held_units": 12000,
  "available_units": 4988000,
  "last_entry_at": "2026-10-14T13:02:15+00:00"
}

Reading this as a model? llms.txt indexes every page, and each page has a “Copy for AI” control.