Error codes¶
Use this page to read a refusal, decide what to do about each code, and know which requests may simply be sent
again. It is a reference — search it by code (Ctrl+F) rather than reading it top to bottom. You need: your
backend's HTTP client.
Every code comes from the module's own code (refuse, oauthError, the intent/refund/settlement failure_codes).
Codes are only ever added — decide an unknown one by its HTTP status (4xx — fix the request, 5xx — retry).
Response shapes¶
The platform refuses in two shapes; both SDKs (Node GeregeError, Go *miniapp.APIError) read both.
| Shape | Where | Example |
|---|---|---|
| OAuth (RFC 6749 / 6750) | client and token authentication (invalid_client, invalid_token, insufficient_scope) and every refusal of POST /oauth/token |
{"error": "invalid_grant", "error_description": "…"} |
| Module | every other refusal | {"error": "<Mongolian message>", "code": "<code>", …extras} |
| Plain | 404 (not found, another app's, or the other environment's), 429 (rate limit), an unexpected 500 |
{"error": "not found"} — no code |
{
"error": "Терминалд өөр нээлттэй төлбөр байна",
"code": "terminal_busy",
"payment_intent": "pi_T3rm1nalVend0000000000Ab"
}
- Branch on
codein your program.erroris a Mongolian message that is safe to show a person — never branch on it (the wording may change). - Extra fields:
details(validation_error's[{path, message}]),payment_intent(the intent as it is now, or, forterminal_busy, the open intent's id),status(not_cancelable,not_claimable),retry_after(already_claimed),scopes,app(consent_required). - The SDKs give a response without
codethe codehttp_<status>(http_404,http_429,http_500). - An OAuth-shaped
401/403carries aWWW-Authenticateheader (Bearer error="invalid_token",Bearer error="insufficient_scope", scope="payments").
Should I retry? Network errors, 429, 5xx and 202 (still in progress — read it again) are safe to send again
with the same Idempotency-Key (err.retryable in Node, apiErr.Retryable() in Go). Repeating a 4xx
unchanged gives the same answer — fix the cause first. The «Retry» column below says so for each code.
Authentication (OAuth)¶
| HTTP | code | When | What to do | Retry |
|---|---|---|---|---|
| 401 | invalid_client |
wrong client id/secret; the key was revoked, has expired, or was rolled and its overlap is over (indistinguishable); no Basic credential on a route that needs one (terminals, sub-merchants, statements, endpoints, events); the app is suspended/disabled; the live key of an app not yet approved |
check the credential and its environment; the key's and the app's state in the console | no |
| 401 | invalid_token |
the person's access token is missing, wrong, expired or revoked | refresh, or have the person open the app again | after refreshing |
| 403 | insufficient_scope |
the person's token or the app lacks the payments consent scope (WWW-Authenticate: Bearer …); or the API key lacks the route's scope — required_scope names it (WWW-Authenticate: Basic …). Since 1.13.0 the module's code: "insufficient_scope" comes beside OAuth's fields |
consent: add payments to the manifest and submit for review; key: a new key with that scope |
no |
| 403 | ip_not_allowed |
the caller's address is not in the key's IP allowlist (ip) — OAuth shape |
fix the allowlist | no |
| 400 | invalid_request |
token: the body could not be read, no grant_type, credentials both in Basic and in the body |
fix the request | no |
| 400 | unsupported_grant_type |
grant_type is neither authorization_code (launch_code) nor refresh_token |
no | |
| 400 | invalid_grant |
the launch code / refresh token is wrong, expired, used or another app's. Presenting a used refresh token again revokes the whole chain | a new launch | no |
| 500 | server_error |
a platform fault (token endpoint, token lookup) | wait and retry | yes |
More: Mini app launch & tokens; the console's key codes — «API keys and scopes» below.
Requests and idempotency¶
| HTTP | code | When | What to do | Retry |
|---|---|---|---|---|
| 400 | invalid_request |
the body could not be read — not JSON, too large, or naming a field the route does not take | fix the request | no |
| 400 | validation_error |
a field is wrong; details[].path names it (amount, description, expires_in, return_url, terminal_id, min_age, capture_within, account …). An amount above the operator's max_amount for the app is one too (path: "amount") |
read details and fix |
no |
| 400 | idempotency_key_required |
Idempotency-Key is missing or not 1–128 printable ASCII characters without spaces |
add a key | no |
| 409 | idempotency_conflict |
the key was used before with a different body (for a person, a different account) | a new key for a new operation; for a retry, the exact same body | no |
| 400 | invalid_status · invalid_cursor · invalid_limit |
a list's status, after/cursor or limit (1–100) is wrong |
fix the parameter | no |
| 404 | — (http_404) |
no such object, or it is another app's / the other environment's (a live intent read with a test key) | check the id and the key's environment | no |
| 429 | — (http_429) |
rate limit (Limits) | back off (e.g. 1, 2, 4 … seconds) | yes |
| 500 | — (http_500) |
an unexpected platform fault | with the same Idempotency-Key |
yes |
The idempotency rules: API conventions.
Payment intents¶
POST /payment-intents, GET …, …/capture, …/cancel, …/unclaim. Statuses and transitions:
Payment intent.
| HTTP | code | When | What to do | Retry |
|---|---|---|---|---|
| 400 | presentment_required |
client credentials (Basic) without presentment, or with app |
qr · redirect · terminal; an app intent is created with the person's token |
no |
| 400 | description_not_allowed |
the description holds a web address, an e-mail address or a phone number (details says which) — the person reads it in the payment sheet just before Face ID (1.13.0) |
write the order's summary; your own number goes in reference |
no |
| 400 | validation_error (path: "presentment") |
a person's token creating qr/redirect/terminal |
claimless intents take client credentials | no |
| 503 | payments_unavailable |
live, and core banking is not enabled on this installation | later | yes |
| 409 | payouts_not_configured |
live, and the app has no payout account, or the sub-merchant's holder has not approved one | the operator / a payout link | no |
| 503 | pay_url_unavailable |
the installation cannot make pay links or terminals (no public origin configured) | the operator | yes (later) |
| 409 | terminal_busy |
the terminal already has an open, unclaimed intent in this environment — payment_intent is its id. On unclaim: the terminal opened a new basket meanwhile |
cancel that intent and try again |
after canceling |
| 409 | terminal_disabled · sub_merchant_disabled |
the terminal / sub-merchant is disabled | PATCH … {status: "active"} |
no |
| 409 | agreement_required |
live, with the go-live rule on (1.14.0): the organisation has not signed the merchant agreement in force with eID | console → Agreements | once signed |
| 409 | business_profile_required · business_profile_rejected · business_profile_changed |
live, rule on: the business profile was never approved, was rejected, or the registered legal name/register differs from the one approved | console → Business profile | once approved |
| 409 | business_restricted |
live: the operator paused the organisation's new live payments (refunds, captures and settlements go on) | the operator | once resumed |
| 409 | business_limit_exceeded |
live: the tier's limit — limit is per_payment (one intent) or daily (live intents created today in Ulaanbaatar, open ones included), with max and used |
a smaller amount / tomorrow / ask for a higher limit in the console | once raised |
| 409 | not_capturable |
only a manual intent in requires_capture can be captured (payment_intent attached) |
read payment_intent.status and decide |
no |
| 409 | expired |
the capture window (capture_before) has passed — the money went back to the person (payment_intent attached) |
close the order | no |
| 422 | payment_failed |
the ledger refused the capture; the hold is released (payment_intent.failure_code is capture_failed) |
close the order | no |
| 409 | in_progress |
a ledger call for this intent is in flight | wait briefly and GET |
yes |
| 409 | not_cancelable |
the intent's state cannot be canceled (status attached) |
if paid, refund | no |
| 409 | not_unclaimable |
only a claimed, unconfirmed claimless intent can be released (payment_intent attached) |
wait, or cancel |
no |
| 500 | server_error |
the terminal / sub-merchant could not be read | retry | yes |
202 Accepted is not an error: the ledger's answer to the capture is not known yet (status: "processing"). Read it
with GET a little later or wait for the webhook — there is no need to capture again (doing so is still safe).
Refunds¶
POST /payment-intents/{id}/refunds (Refunds).
| HTTP | code | When | What to do | Retry |
|---|---|---|---|---|
| 409 | not_refundable |
only a succeeded or partially_refunded intent can be refunded |
no | |
| 409 | refund_window_expired |
more than 90 days have passed since the payment | settle with the person yourself | no |
| 400 | validation_error |
amount < 1, or refunds would total more than was paid; reason > 255 |
no | |
| 400 · 409 | idempotency_key_required · idempotency_conflict |
as above; the key is unique per intent | no | |
| 503 | payments_unavailable |
live, core banking not enabled | yes |
A refund the ledger refuses is not an HTTP error: the answer is 201 with the refund's status: "failed", a
failure_code from the table below, and a refund.failed webhook. When the answer is not known yet it is 202 with
status: "pending" — the platform finishes it and sends the webhook. The same key again answers 200 with that
refund.
Merchant registry — terminals, sub-merchants, payout links¶
/terminals…, /sub-merchants… — client credentials (Basic) only. Batch and sticker-PDF codes — «Terminal tools»
below.
| HTTP | code | When | What to do | Retry |
|---|---|---|---|---|
| 403 | live_only |
a test key creating or changing a terminal / sub-merchant, or requesting a payout link (test keys only read — the registry is one for both environments) | the live key or the console | no |
| 409 | reference_taken |
another terminal of the app has this reference |
another reference |
no |
| 400 | invalid_sub_merchant |
sub_merchant_id is not this app's |
no | |
| 400 | code_immutable |
an attempt to change a sub-merchant's code |
register a new sub-merchant | no |
| 409 | sub_merchant_disabled |
a payout link for a disabled sub-merchant | enable it first | no |
| 400 | validation_error (path: "account") |
the payout account was not found, is inactive or is not MNT | no | |
| 422 | account_unsupported |
the account's holder is neither a Gerege App person nor an organisation | another account | no |
| 422 | no_admin |
an organisation's account with no admin to ask | the organisation appoints an admin | no |
| 503 | directory_unavailable |
the organisation's admins could not be read just now — "could not tell", never "none" | retry | yes |
| 502 | core_unavailable |
core banking did not answer | retry | yes |
| 503 | payments_unavailable · pay_url_unavailable |
core banking / the merchant registry is not enabled on the installation | yes (later) |
More: Pay with Gerege App, Payout accounts.
Settlements¶
GET /settlements, GET /settlements/{id} (Settlements).
| HTTP | code | When | What to do | Retry |
|---|---|---|---|---|
| 503 | settlements_unavailable |
the installation has no settlement store | yes (later) | |
| 400 | invalid_status · invalid_cursor · invalid_limit |
status is not pending/paid/failed, a bad cursor, limit not 1–100 |
no | |
| 404 | — (http_404) |
no such settlement, or read with a test key (test mode never has settlements; the list is empty) | the live key | no |
failure_code¶
Not a refusal — a field on the object that says what happened.
Intent (payment_intent.failure_code)¶
failure_code |
Meaning | Intent |
|---|---|---|
insufficient_funds |
the balance is short | failed — the person may pay again from another account |
limit_exceeded |
a transaction limit of the person's account was exceeded | failed, payable again |
account_unavailable |
the chosen account cannot pay (closed, not the person's) | stays requires_confirmation, no webhook |
authorization_expired |
a manual intent's hold window (capture_before) ran out |
expired |
expired |
the intent ran out of time during confirmation | expired |
merchant_unavailable |
the app stopped taking payments (suspended, no payout) — the hold was released | failed |
capture_failed |
the ledger refused the capture — the hold was released | failed |
hold_mismatch |
the ledger's hold did not match the intent and was not used | failed |
captured_after_release |
the hold had already been captured when it was released — the platform reconciles by hand | (status unchanged) |
test_declined |
test mode, an amount ending …01 (Test amounts) |
failed |
payment_failed |
any other refusal | failed |
A failed intent is payable again, as in OMAS (a new attempt places a new hold).
Refund (refund.failure_code)¶
failure_code |
Meaning | What to do |
|---|---|---|
insufficient_funds |
the payout account has already spent the payment | top the account up and refund again with a new key |
settlement_shortfall |
the app's clearing account cannot cover it (the payment was already settled) | contact the operator |
refund_rejected |
any other refusal | contact the operator |
Settlement (settlement.failure_code)¶
failure_code |
Meaning |
|---|---|
idempotency_conflict |
the ledger saw the same key with other lines — the operator investigates |
insufficient_funds |
the clearing balance disagreed with the records |
destination_unavailable |
the payout account is closed or frozen — the operator changes the payout and settles again |
limit_exceeded |
a ledger limit |
rejected |
any other refusal |
The operator retries or voids a failed settlement (its lines then go into the next one).
The person's side (for information)¶
Gerege App (iOS) receives these; your backend never does, but they explain what the person saw.
| HTTP | code | When |
|---|---|---|
| 404 | unknown_payment · unknown_terminal · nothing_pending |
the pay_url token is not known (or is a test payment this person may not see) · no such or inactive terminal · nothing to pay at the terminal now |
| 409 | already_claimed |
somebody else is paying it (retry_after: 90 — an unconfirmed claim lapses after 90 seconds) |
| 409 | not_claimable |
the intent cannot be paid now (status attached) |
| 410 | expired |
the intent ran out of time when it was claimed |
| 429 | too_many_open_claims |
the person already holds 3 unconfirmed claims |
| 409 | merchant_unavailable |
the app does not take payments now (suspended, disabled, no payout) |
| 403 | verification_required · age_restricted |
the app or the intent's min_age needs an eID-verified person · under age, or the age cannot be read |
| 409 | already_processed · in_progress · expired |
confirmation: already decided · in flight · ran out of time |
| 409 · 403 · 422 | insufficient_funds · limit_exceeded / account_unavailable · test_declined / payment_failed |
confirmation outcomes (the intent attached) |
| 409 | not_pending · account_unavailable |
payout link: already answered or expired · the account is no longer the person's (or their organisation's) |
| 428 | consent_required |
launch: the person has not granted the scopes (scopes, app attached) |
| 409 · 403 · 400 | maintenance · channel_unavailable · invalid_path / invalid_channel / invalid_scope |
launch: the app is in maintenance · not offered on this channel · a bad path, channel or scope |
Bridge codes: Bridge.
Developer console and sandbox¶
Codes of the console (/api/v1/miniapp/dev/…) and of the operator's review — your server integration never receives
them; the console's screens show them.
| HTTP | code | When |
|---|---|---|
| 409 | slug_taken |
another app has this slug |
| 409 | sandbox_only |
a sandbox organisation (a person's, with no legal entity): no live key, live secret, review submission, agreement, payout link or app payout — test mode works in full (Sandbox) |
| 400 | sandbox_slug |
a sandbox's app slug begins with sbx- (^sbx-[a-z0-9-]{3,27}$), and no other organisation may use sbx- |
| 503 | sandbox_unknown |
the platform could not say whether the organisation is a sandbox — nothing live was done; try again |
| 409 · 400 | version_conflict · version_required |
somebody changed it meanwhile — read again and redo the change · the change must name the version it was made against |
| 409 | locked_for_review · needs_review · status_locked · suspended |
an app in review cannot change · a sensitive field of an approved app (kind, return_hosts …) — draft or rejected only · the status cannot change this way · the app is suspended |
| 400 | unknown_field · invalid_field · invalid_environment · invalid_range · invalid_app_id |
an unknown PATCH field · wrong type · environment not live/test · analytics dates (≤ 366 days) · app_id not a UUID |
| 400 · 409 | not_ready · not_submittable |
submit: requirements missing (details) · not in a state that can be submitted |
| 409 | merchant_no_manifest |
a merchant has no manifest to check |
| 503 | no_secret_key |
the installation has no key to seal the webhook secret with — it is never stored in plaintext |
| 409 | no_webhook |
no webhook URL / endpoint (send again, webhook.test) — the other endpoint codes are under «Webhook endpoints and the events API» below |
| 404 · 409 · 503 | user_not_found · too_many_test_users · directory_unavailable |
the test user is not an active member of your organisation · > 50 · the member list cannot be read just now |
| 400 | idempotency_key_invalid |
the sandbox test intent's Idempotency-Key is not 1–128 characters |
| 409 | merchant_unavailable · already_processed · expired |
confirming a test intent |
| 503 | settlements_unavailable |
no settlement store |
| 422 · 422 | account_not_owned · account_unavailable |
the app's payout (PUT …/apps/{id}/payout): the account is not your organisation's — the operator sets it · no such account, not active, or not MNT |
| 403 | admin_required · impersonated |
only the organisation's admin, in their own session, sets the app's payout |
Operator codes (operator console only): invalid_review_status, not_in_review, already_suspended,
note_required, not_draft, not_archived (layouts), settlements_off, settlement_open, refund_pending,
not_failed, not_retryable, rule_clash, rule_in_effect, ledger_seg_taken, gl_unavailable.
The codes of withdrawing from a wallet to a bank (/api/v1/bank/…): Withdrawals to a bank.
SDK errors¶
Node (@gerege/miniapp-node) |
Go (sdk/go) |
|
|---|---|---|
| API refusal | GeregeError — status, code, message, requestId, details (the extra fields) |
*miniapp.APIError — StatusCode, Code, Message, RequestID, Details ([]Issue), Extra |
| Token endpoint | GeregeOAuthError (error, error_description) |
*APIError (Code = OAuth's error) |
| Network / timeout | GeregeError with status: 0, code: "network_error" |
a wrapped error (not an APIError) |
| Retry? | err.retryable — 0, 429, 5xx |
apiErr.Retryable() — 429, 5xx |
| Before the request | TypeError (key, id or credential missing) |
ErrIdempotencyKey, ErrPresentment, ErrMissingID, ErrMissingAccount, ErrNoCredentials |
| Webhooks | GeregeWebhookError.reason |
ErrMissingHeader, ErrMalformedHeader, ErrTimestamp, ErrSignatureMismatch, ErrInvalidPayload, ErrNoSecret |
The sections below hold the codes of the developer platform's new APIs (keys, endpoints and events, the dashboard and statements, terminal tools, agreements) — each change that adds an API writes its own section.
API keys and scopes¶
API keys (1.8.0): client id gk_live_… / gk_test_…, secret gks_live_…_<checksum> (the old mapp_… +
gma_cs_… pair = the «default key», with every scope of its environment). Every server route names the scope it
needs in the OpenAPI document's x-gerege-scope.
| HTTP | code | When | What to do |
|---|---|---|---|
| 401 | invalid_client |
wrong secret; the key was revoked, has expired, or was rolled and its overlap is over (indistinguishable) | check the key's state in the console |
| 403 | insufficient_scope |
the key lacks the route's scope — required_scope names it (WWW-Authenticate: Basic error="insufficient_scope", scope="…") |
a new key with that scope (scopes do not change) |
| 403 | ip_not_allowed |
this address (ip) is not in the key's IP allowlist |
fix the allowlist |
| 403 | live_only |
a test key writing a terminal / sub-merchant / payout link (checked before the scope) | the live key or the console |
Console (/dev/apps/{id}/keys…): 400 validation_error (details[].path: name, environment, scopes —
unknown, duplicate, live-only on a test key, preset, allowed_ips, expires_at, overlap_seconds, reason),
400 scopes_immutable (a PATCH trying to change the scopes), 403 permission_required (permission:
miniapp.develop — test, miniapp.keys.live — live), 403 fresh_sign_in_required (within_seconds),
409 name_taken, 409 too_many_keys (20 active per environment), 409 key_not_active (status: revoked / expired /
rolled), 409 suspended, 503 fresh_sign_in_unavailable (the installation could not tell when the member signed in — no live key is
created, rolled or re-addressed, no live secret issued by POST /dev/apps/{id}/credentials; try again later),
409 sandbox_only (a sandbox holds no live key), 400 invalid_environment / invalid_cursor / invalid_limit (lists).
Since 1.13.0 a live key or live secret needs miniapp.keys.live and a sign-in within the last ten minutes
(403 fresh_sign_in_required); IP allowlists use the address the core resolves (behind its trusted proxies).
Webhook endpoints and the events API¶
/webhook-endpoints (client credentials, the credential's environment) and the console's
/dev/apps/{id}/webhook-endpoints, /dev/apps/{id}/webhook/…; /events (client credentials).
| HTTP | code | When | What to do |
|---|---|---|---|
| 409 | too_many_endpoints |
the environment already has 5 endpoints (endpoint №1 included) | delete or merge one |
| 403 | live_only |
a test key called /webhook-endpoints — webhooks:write is live keys' only |
test endpoints are the console's |
| 403 | insufficient_scope |
the key lacks webhooks:write / events:read (required_scope) |
a key with that scope |
| 403 | environment_mismatch |
the key tried to create / list an endpoint of the other environment | use that environment's key |
| 409 | environment_mismatch |
console: redelivering a live event to a test endpoint (or the other way round) | pick an endpoint of the same environment |
| 400 | validation_error |
url (https, a public address), environment, description ≤ 200, event_types (known, no duplicates, ≤ 20; not webhook.test), status |
details[].path |
| 409 | suspended · locked_for_review |
the app is suspended; endpoint №1's URL/secret of an app in review | wait for the review |
| 409 | no_webhook |
rotating endpoint №1's secret after the app's webhook was removed; the console's test/redelivery has no endpoint | add an endpoint |
| 503 | no_secret_key |
the installation has no key to seal secrets with | contact the operator |
| 400 | invalid_type |
the type filter is not an event type |
|
| 400 | test_event_not_listed |
/events?type=webhook.test — the console's test send goes to the endpoint it names and is never in /events |
the console's delivery log |
| 400 | invalid_endpoint |
console: the endpoint_id filter is not an endpoint's id |
|
| 400 | invalid_cursor · invalid_limit · invalid_environment |
a list parameter is wrong | |
| 400 | unknown_cursor |
/events?after= is not this app's and environment's event, or is older than 30 days (purged) |
start again with created_gte |
| 400 | invalid_created_gte |
created_gte is not RFC 3339 |
|
| 404 | — (http_404) |
no such endpoint/event, or it is another app's / environment's |
An endpoint failing every delivery for 24 hours gets the organisation's admins notified (miniapp.webhook.failing);
for 5 days it becomes status: auto_disabled (miniapp.webhook.disabled) — fetch what it missed from /events, then
PATCH {"status":"active"}.
Dashboard, statements and simulation¶
The console's payments dashboard (/dev/payments…, /dev/refunds, /dev/statements, /dev/audit), the merchant
backend's GET /statements and the sandbox's simulation (…/simulate, …/simulate-tap).
| HTTP | code |
When |
|---|---|---|
| 400 | idempotency_key_required |
a dashboard action (capture, cancel, unclaim, refunds) without an Idempotency-Key (1–100) |
| 400 | validation_error |
details[].path: reason (required on a refund, ≤ 255), amount, app_id, terminal_id, sub_merchant_id, created_gte/created_lt (RFC 3339), amount_gte/amount_lte, q (≤ 128), presentment, action, test_user_id |
| 400 | invalid_status · invalid_environment · invalid_cursor · invalid_limit · invalid_format · invalid_range |
filters, paging, format (json | csv), from/to (YYYY-MM-DD, ≤ 366 days) |
| 403 | permission_required |
permission: miniapp.payments (actions), miniapp.finance (CSV, statements) or miniapp.develop (simulation) |
| 403 | fresh_sign_in_required |
a live refund of 1 000 000₮ or more from the dashboard — sign in again within within_seconds (600) and repeat |
| 503 | fresh_sign_in_unavailable |
the installation could not check a fresh sign-in — the large refund is not made |
| 409 | not_capturable · expired · not_cancelable · in_progress · not_refundable · refund_window_expired · idempotency_conflict · not_unclaimable · terminal_busy |
from the app backend's own path — the same meaning as under «Payment intents» and «Refunds» above |
| 409 | live_intent |
simulation is for test intents only |
| 409 | not_a_test_user |
test_user_id (or the caller) is not one of the app's test users or its owner |
| 409 | not_claimless · not_claimed · not_expirable |
a claim of an app intent; confirm/decline of an unclaimed one; expire of a finished intent |
| 409 | already_claimed (retry_after) · already_processed · merchant_unavailable · terminal_disabled |
as for a real claim and confirmation |
| 404 | nothing_pending |
simulate-tap: no open test intent at the terminal |
| 410 | expired |
the simulated claim — the intent ran out of time |
| 429 | too_many_open_claims |
the test user holds 3 unconfirmed claims |
| 422 | statement_too_large |
the statement is over 100 000 rows (max_rows) — shorten the range |
| 503 | dashboard_unavailable · pay_url_unavailable · payments_unavailable |
the installation's configuration / a passing fault — try again |
Terminal tools¶
POST /terminals/batch, POST /terminals/print-sheet (the console also has GET …/print-sheet and
GET …/terminals/{tid}/stats):
| HTTP | code |
When |
|---|---|---|
| 400 | too_many_terminals |
the batch is empty or has more than 500 rows (max); printing without terminal_ids when more than 500 terminals are active |
| 400 | invalid_csv |
Content-Type: text/csv could not be read: no header row, no label column, an unknown or duplicate column (columns), a broken row (line) |
| 400 | validation_error |
details[].path = terminals[i].label … — one bad row refuses the whole batch and nothing is written; printing: layout, locale, crop_marks, terminal_ids[i] (a wrong id, a duplicate, not found, a disabled terminal) |
| 403 | live_only |
a batch with a test key — the registry is written only by the live key or the console (printing and reading statistics work in test too) |
| 409 | no_terminals |
no active terminal to print |
| 400 | invalid_range / invalid_environment |
the statistics' from/to (YYYY-MM-DD, ≤ 366 days), environment |
A batch answers 200 with each row's status: created, existing (that reference is registered), failed
(error.code: reference_taken, invalid_sub_merchant, …; the other rows ran), skipped (not attempted because of a
server fault — send the batch again; rows with a reference are not created twice).
Agreements and eID signatures¶
The console's /dev/agreements… and the operator's /review/agreements… (not server-to-server). Starting a signature
needs the gate's miniapp.manage and miniapp.agreements.sign (or admin), checked by the core, and you must be a
representative of that organisation in eID.
| HTTP | code | When | What to do |
|---|---|---|---|
| 202 | — | POST …/{id}/sign: the request reached the phone — {signature_id, verification_code, org_name} |
match the code with the phone's, then PIN2; poll with GET …/signatures/{sid}?wait=20 |
| 409 | signing_unavailable |
the installation has no eID signing path (no SIGNATURE right on the RP, no PDF signing) | contact the operator |
| 503 | no_secret_key |
the installation has no key to seal secrets with | the operator |
| 403 | not_representative |
in eID's records you are not a current representative of this organisation | a representative (the director, …) signs; fix the record at the state registry |
| 403 | permission_required |
no miniapp.agreements.sign (permission) |
an admin grants it |
| 403 | impersonated |
a session signed in as somebody else | use your own session |
| 409 | no_eid |
no eID is linked to your account | sign in with eID to link it |
| 409 | no_registration_number |
the organisation's profile has no registration number (always so in a sandbox) | fill in the profile |
| 409 | already_signed |
this version is already signed (signature_id) |
— |
| 409 | signature_running |
a ceremony is open (signature_id, verification_code) |
check your phone or wait for it to end |
| 409 | agreement_superseded |
an old version — only the current or the upcoming one can be signed | sign the current one |
| 409 | not_signed |
…/document: the signature has not finished yet |
poll |
| 502 | signing_failed |
the eID request could not be started | try again |
| 503 | agreements_unavailable |
the installation has no agreements store | — |
| 400 | invalid_wait |
wait is not 0–20 |
The ceremony's state (signature.status): running → completed | failed | expired | rejected (eID's).
failure: rejected, expired, failed, no_answer (eID did not answer in 2 hours), session_lost,
stamp_unavailable (no timestamp/OCSP for 24 hours), organisation_mismatch, not_a_pdf, no_certificate,
file_store, signature_unreadable. After failed/expired/rejected a new one may be started.
Operator: 400 validation_error (details), 400 invalid_kind, 409 version_taken, 409 not_draft (editing,
deleting or republishing a published one), 409 draft_text (in production, a template text still marked as a draft
not reviewed by a lawyer). A sandbox organisation cannot start a signature: 409 sandbox_only (1.13.0).