Verified traps
12.1 Unknown fields are ignored - but the response now tells you
A body with a misspelled field name still returns 201 Created, and the payment is still created
without that feature. What changed on 2026-08-17 is that the response carries a warnings array
naming every field that was dropped:
{ "amount": 100, "service_fee": true } <- wrong name
-> 201 Created
"service_fee_amount": 0 <- the fee pass-through was NOT applied
"warnings": ["Campo desconocido ignorado: service_fee"]
Read that array. It is the cheapest correctness check this API offers.
Behaviour of the field, verified:
| Situation | warnings |
|---|---|
| Clean payload | absent from the response - not null, not [] |
| One unknown field | array with one string |
| Several | array with one string each, one per field |
Accept-Language: en |
translated: "Unknown field ignored: save_card" |
Because the text is localised, branch on the array being non-empty, never on its wording.
This used to be the most dangerous failure mode of the API, because the result looked successful,
and it is still dangerous for code that ignores the new field - especially AI-generated code, which
writes other gateways' names (card_token, payment_method, customer_id, amount_cents) with
full confidence.
Defence, in order of strength:
- Validate before sending against the schema in
openapi.yaml, which declaresadditionalProperties: falseprecisely to catch this. A request that never leaves your process cannot create a wrong payment. - Log
warningson every response, and fail loudly in development. An empty deployment log is the only proof your field names are right. - Verify the computed fields (
sales_tax_amount,service_fee_amount,total,reusable) against what you intended. Warnings catch a name you invented; they do not catch a value the API computed differently from what you assumed.
12.2 amount has no ceiling and coerces types
The API accepts absurd amounts (11 digits) and numeric strings ("150.00"). Enforce a sane maximum
before sending.
12.3 Three different kinds of 404 - tell them apart by code, not by the message
Every error response carries a stable code next to the localized message. On a 404 it is the
only reliable way to know what actually went wrong:
code |
Meaning | Fix |
|---|---|---|
payment_not_found |
The route exists and the identifier is well formed; no such record for this key. Usually an id from the other environment. | Use the key from the same environment as the id. |
transaction_id_expected |
The route exists and rejected the identifier: you passed the numeric payment id (or anything that is not a UUID) where the transaction UUID belongs. |
Send transaction_id, not id (13.1). |
not_found |
The path is not in the router at all. Malformed URL, missing /v1, or a removed endpoint. |
Fix the URL. No change of identifier will help. |
Telling them apart saves hours, and each points at a different fix.
Do not classify these by message text. Two reasons, both verified:
- Until 2026-08-21 the numeric-id mistake returned the routing 404, so the whole ecosystem learned
to test for
"could not be found". It no longer returns that. Code carrying that test now files the single most common identifier mistake under "no such payment" and sends the developer hunting for a bad id instead of a bad identifier type. payment_not_foundandtransaction_id_expectedmessages are localized byAccept-Language; only thenot_foundprose stays English. A text match that works in one language breaks in the other.
12.4 Timestamps are Honduras local time with no offset
expires_at, created_at and paid_at come back as YYYY-MM-DD HH:MM:SS in Honduras time
(UTC-6), with no timezone marker in the string. Parsing them as UTC shifts every value by six
hours - enough to make a future expiry look expired, or an order look paid before it was created.
Attach the offset explicitly when parsing.
12.5 A customer-selected tip is not in the total you were given
With tip_customer_selectable, the total returned at creation excludes the tip because the
customer has not chosen it yet. The amount actually charged can therefore be higher than the total
you stored. Reconcile against the webhook payload or a fresh lookup, never against the creation
response.
12.6 metadata can come back as an empty array
When no metadata was sent, the API may return "metadata": [] instead of {}. Strict
deserializers that expect an object will throw. Treat both shapes as "no metadata".
12.7 Do not hardcode the checkout domain from the official docs
The checkout_url returned is on
aura.roki.systems/pay/link/{slug}. Never hardcode or allow-list a checkout domain - always
redirect to the exact checkout_url the API returned.
12.8 success_url accepts http://
Although the official documentation requires HTTPS, the API accepts unencrypted URLs. Use HTTPS anyway - it is a return URL in a payment flow.
