Charges (trusted apps)
Trusted apps can book single items onto a user's open monthly bill of the app – e.g. each item a shipping app sends: one line per charge, no subscription, charged with the bill on the 1st like every other line, with invoice and VAT by Anikeen ID.
For a separate one-off purchase charged on its own, create an order instead.
Requirements:
- Anikeen has to enable charging for your app and sets a maximum per charge (net).
- The user must be connected to your app.
- An app token (client credentials) with
billing:client.
The user's controls are their billing limit (0.00 stops all charges) and removing your app's access.
Endpoints
| Method | Path | |
|---|---|---|
POST | /api/v1/client/users/{user}/charges | book; 201, an idempotent retry 200 |
GET | /api/v1/client/users/{user}/charges/{charge} | the charge |
POST | /api/v1/client/users/{user}/charges/{charge}/void | void it while its bill is open; reason? (at most 255 characters) |
Booking a charge
curl -X POST https://id.anikeen.com/api/v1/client/users/01jexample/charges \
-H 'Authorization: Bearer APP_TOKEN' \
-H 'Accept: application/json' -H 'Content-Type: application/json' \
-d '{
"idempotency_key": "shipment-123",
"name": "Signed card",
"type": "physical",
"quantity": 2,
"unit_price": "4.00",
"reference": "order-77"
}'| Field | |
|---|---|
idempotency_key | required, at most 255 characters; unique per app and user |
name | required, at most 255 characters: the line on the bill |
description | optional, at most 255 characters |
type | digital (default), physical or physical-reduced: the VAT treatment |
quantity | required integer 1–10000 |
unit_price | required, net per piece, > 0, at most 100000, up to 6 decimals; quantity × unit price rounded to cents must be at least 0.01 |
currency_iso | EUR only (default) |
group | optional: null or a group in which your app already bills the user |
occurred_at | optional date, not in the future, at most 31 days ago (default: now): the service date |
reference | optional, at most 255 characters: your own ID, not on the bill |
metadata | optional object, at most 20 keys (at most 40 characters each), string values of at most 500 characters |
image_url | optional http(s) URL, copied like order item pictures |
reference and metadata are your data about the user, kept with the charge and not shown on the bill: put nothing sensitive there.
{
"data": {
"object": "charge",
"id": "01J…",
"user_id": "01j…",
"status": "open",
"idempotency_key": "shipment-123",
"name": "Signed card",
"description": null,
"type": "physical",
"quantity": 2,
"unit": "piece",
"unit_price": "4.000000",
"net_amount": "8.00",
"gross_amount": "9.52",
"currency_iso": "EUR",
"group": null,
"occurred_at": "…",
"reference": "order-77",
"metadata": {},
"order_id": "01J…",
"voided_at": null,
"void_reason": null,
"created_at": "…"
}
}status is open (its bill is open), billed (the bill was closed) or voided. gross_amount is what the charge added to the bill, order_id the monthly bill it is on.
$anikeen = app(\Anikeen\Id\AnikeenId::class);
$charge = $anikeen->createChargeForUser($user->anikeen_id, [
'idempotency_key' => "shipment-{$shipment->id}",
'name' => 'Signed card',
'type' => 'physical',
'quantity' => 2,
'unit_price' => '4.00',
'reference' => "order-{$shipment->order_id}",
]);
$charge->id(); // keep it to void the charge later
$charge->grossAmount(); // "9.52"
$charge->orderId();
$charge->status(); // open, billed or voided
$anikeen->findChargeForUser($user->anikeen_id, $chargeId);
$anikeen->voidChargeForUser($user->anikeen_id, $chargeId, 'Parcel lost');Rules
- Idempotency: retrying with the same key and the same fields returns the existing charge (
200instead of201), even after the limit was reached. The same key with other fields is409 idempotency_key_conflict(withcharge_id). Use one key per item you charge for. - Maximum: a charge above your app's maximum (net) is
409 charge_exceeds_maximumwithmax_charge_net. - Group:
null, or a group your app already bills the user in (a running subscription or an open monthly bill of your app); others are409 group_not_allowed. - Billing address: the user needs one (
409 billing_address_missing). - Billing limit: a charge that would take the user's open bills above their billing limit is refused with
409 billing_limit_reached. Exactly reaching the limit is allowed. - Voiding removes the line while the bill is open (an already voided charge is returned as it is). Once the bill was closed:
409 charge_already_billed; apps can't refund billed charges.
A refused charge
HTTP/1.1 409 Conflict
{
"message": "The customer's billing limit is reached.",
"error": "billing_limit_reached",
"limit": "100.00",
"used": "97.40",
"remaining": "2.60",
"remaining_net": "2.18",
"required": "4.76",
"currency": "EUR",
"resets_at": "2026-12-01T02:00:00Z"
}used and remaining are before the refused charge, required is its gross amount. A 409 while room remains refuses only this charge: decide from remaining (e.g. a smaller one).
use Anikeen\Id\Exceptions\ApiException;
try {
$charge = $anikeen->createChargeForUser($user->anikeen_id, $attributes);
} catch (ApiException $e) {
if ($e->isBillingLimitReached()) {
$e->body()->remaining; // room left before this charge, e.g. "2.60"
$e->body()->required; // the refused charge, gross
$e->body()->resets_at; // when the limit has room again at the latest
return $shipment->holdForBillingLimit();
}
match ($e->error) {
'charge_exceeds_maximum' => report($e), // $e->body()->max_charge_net
'group_not_allowed' => report($e),
'billing_address_missing' => $shipment->askForBillingAddress(),
default => throw $e,
};
}Errors
| Status | error | When |
|---|---|---|
| 401, 403 | – | no or an invalid token |
| 403 | client_not_allowed | not a trusted app with its own token |
| 403 | charges_not_enabled | charging is not enabled for your app (also for reads and voids) |
| 404 | user_not_found | unknown, deleted or not connected user |
| 404 | charge_not_found | no such charge of this user and app |
| 409 | billing_limit_reached | above the user's billing limit (body above) |
| 409 | charge_exceeds_maximum | above your app's maximum (+ max_charge_net) |
| 409 | group_not_allowed | your app doesn't bill the user in that group |
| 409 | idempotency_key_conflict | the key was used for another charge (+ charge_id) |
| 409 | billing_address_missing | the user has no billing address |
| 409 | charge_already_billed | void after the bill was closed (+ charge_id) |
| 422 | – | validation (errors) |
| 429 | too_many_requests | rate limited per app and user, with Retry-After |
On the bill
Charge lines are ordinary lines of your app's monthly bill: invoices show them with net price, VAT and the service date. App orders list them with charge_id, also in the order webhooks.