Payments
Payment intents and idempotency
A payment intent makes the economic terms of a transaction immutable before the user interacts with the widget. This prevents a malicious client from changing the amount, destination account or payment reference in the frontend.
Creation parameters (POST /payments/?action=create)
1. Standard merchant model (own beneficiary)
In this model, the merchant specifies all collection details:
| Field | Type | Required | Description and rules |
|---|---|---|---|
amount_minor |
Integer | Yes | Amount in minor units (cents). For example, 1500 represents 15,00 EUR. |
currency |
String | Yes | Three-letter ISO 4217 currency code. Currently EUR. |
beneficiary |
Object | Yes | Destination account details: name (account holder, 1–140 characters) and iban (valid IBAN without spaces, with its checksum verified). Only these two keys are accepted. |
reference |
String | Yes | Payment reference shown on the bank statement (maximum 140 characters). |
customer_reference |
String | Yes | Internal order or customer identifier (1–128 characters: letters, numbers, ., _, :, -; must begin with a letter or number). |
allowed_origin |
String | Yes | Exact HTTPS origin that will embed the widget (e.g. https://tienda.example.com), without a path. |
allowed_institution_codes |
Array | No | List of permitted institution codes (e.g. ["santander-es", "bbva-es"]). If omitted, any institution in the catalogue is allowed. |
locale |
String | No | Widget interface language. This version only accepts es; any other value returns 422 invalid_locale. |
expected_mode |
String | No | mock or live. Checks that you are calling the intended environment; it does not switch environments. A mismatch returns 409 payment_mode_mismatch. |
The body uses a strict allowlist: sending a field not listed in this table returns 422 invalid_request, as does omitting a required field. The Content-Type: application/json header is mandatory (415 json_required), and the body is limited to 32 KB.
2. Managed profile model (donations / demos)
For regulated use cases or public demos such as cruz_roja_demo, the server enforces the financial rules and fixes the official destination accounts:
| Field | Type | Required | Description and rules |
|---|---|---|---|
profile |
String | Yes | Profile identifier (e.g. cruz_roja_demo). |
institution_code |
String | Yes | Code of the institution selected by the user (from profile-institutions). |
amount_minor |
Integer | Yes | Amount bounded by the profile policies (e.g. 1–100 cents). |
customer_reference |
String | Yes | Your system's own audit reference. |
allowed_origin |
String | Yes | HTTPS origin of the widget. |
locale |
String | No | Widget language (es). |
expected_mode |
String | No | Expected mode (mock or live). |
When profile is specified, the server automatically assigns the official beneficiary and the corresponding payment reference. Do not send beneficiary or reference in managed profile requests.
The Idempotency-Key rule
The Idempotency-Key header is mandatory for every creation request. It must contain 16–128 visible ASCII characters, without spaces (for example, a UUID v4). Its scope is limited to the authenticated company:
- Same key and same body: returns the original, previously created intent with
idempotent_replay: true. No new charge is created and the bank order is not duplicated. - Same key and different body: immediately returns HTTP
409 Conflict. - Same key in another company: belongs to a completely isolated idempotency namespace.
If a creation request is interrupted by a network failure or timeout, retry with exactly the same Idempotency-Key header and the same body. Do not generate a new key for a transient error.
Persistence and lifecycle
- Persist first: The intent is recorded in the database before returning the response or interacting with any bank connector.
- Ephemeral widget token: The response token (
payment.widget.token) has a short lifetime (typically 15–30 minutes) and can only be used from the declaredallowed_origin. - Concurrency control: The system uses optimistic concurrency control and leases to prevent two simultaneous requests from authorizing or changing the same intent.
Common error codes
| HTTP | Code | Cause | Recommended action |
|---|---|---|---|
400 |
idempotency_key_required |
The Idempotency-Key header is missing or malformed. |
Generate a valid key of 16–128 visible ASCII characters, without spaces. |
409 |
idempotency_conflict |
The key has been reused with different payment details. | Generate a new key for different payments, or reuse the identical body. |
409 |
payment_mode_mismatch |
expected_mode does not match the environment you are calling. |
Check whether you are targeting sandbox or production; the deployment sets the mode, not the request. |
401 |
invalid_api_key |
The X-API-Key header is missing or malformed, or the credential does not exist or is inactive. |
Check the credential. Never include it in the frontend. |
403 |
payments_not_allowed |
The API key does not have the PAYMENTS product enabled. |
Contact Wealth Reader support to enable payments for your account. |
403 |
payment_profile_not_allowed |
The managed profile is not enabled for your company. | Ask Wealth Reader to enable the profile. |
429 |
api_limit_reached |
Your credential has exhausted its cumulative call counter. | Retrying or waiting will not resolve this: there is no time window or automatic replenishment. Request a higher limit. |
415 |
json_required |
The Content-Type: application/json header is missing. |
Send the body as JSON. |
422 |
invalid_request |
A required field is missing or an unrecognized field was sent. | Check the parameter table: the body uses a strict allowlist. |
422 |
invalid_institution |
The selected institution is unavailable or invalid. | Query GET /payments/entities/ for valid codes. |
422 |
invalid_amount |
The amount is below the minimum (€0.01) or is not an integer. | Check that you are sending an integer in cents (amount_minor). |
Next step
Continue with Statuses, polling and reconciliation.