Aller au contenu

Merchant isolation and keys

Ce contenu n’est pas encore disponible dans votre langue.

The sandbox, hosted or local, supports independent test merchants. Each API key belongs to one merchant and carries explicit permissions. Never expose it in checkout or payer code. A payment created by another merchant returns 404 even if its UUID is known; idempotency keys are independent between merchants.

Use Authorization: Bearer <test-secret>. GET /v1/keys returns metadata, permissions, revocation and last-used timestamps without secret material. POST /v1/keys creates a key whose permissions must be a subset of the caller’s. POST /v1/keys/{id}/rotate replaces and immediately revokes its predecessor. POST /v1/keys/{id}/revoke invalidates future requests. Key management requires keys:manage.

From OpenAPI 0.47.0, no key grants keys:manage to the keys it creates or rotates (D-034): asking for it, or rotating a key that holds it, returns 403 SCOPE_ESCALATION. Only a credential provisioned outside a key (the sandbox bootstrap key or staff maker-checker issuance) can manage keys here; members manage keys through the portal behind a recent sign-in (see developer controls). Keys another key had given keys:manage have lost it, and one that held nothing else was revoked; from 0.55.0 the account journal shows each by its label.

Save a generated secret when it is returned: it is shown once and stored only as a hash. If a creation response is lost, list and revoke the unused key. If rotation leaves you without a usable key, an Owner, Admin or Developer can issue a replacement in the portal, as can a credential provisioned outside a key or an independently approved staff recovery action. Test and live credentials are separate; live keys are disabled in this sandbox.

Quota headers describe the merchant’s shared per-minute allowance. All of its keys consume that allowance; creating or rotating a key does not reset it. On 429, observe Retry-After. On an uncertain payment-creation response, retry the identical body with the same Idempotency-Key.

From OpenAPI 0.54.0, every refusal answers {"error":{"code":"…"}} with a catalogued code (D-034); 0.55.0 added 406 NOT_ACCEPTABLE, kept the framework’s other refusals at their own status and refused a multipart body as 415. A refusal relayed as an upstream service gave it may also carry message_en and message_fr beside its code. Besides each operation’s documented codes, the refusals the framework answers before an operation runs are:

Status Code When
400 INVALID_REQUEST A missing parameter, or another framework refusal not named here
404 ROUTE_NOT_FOUND A covered prefix without a route
405 METHOD_NOT_ALLOWED A method no operation answers on a covered route; Allow lists the methods it does
406 NOT_ACCEPTABLE An Accept the route cannot produce
415 UNSUPPORTED_MEDIA_TYPE A JSON route’s request body that is not JSON; a multipart body is never parsed
500 INTERNAL_ERROR Only an unexpected failure, logged and never detailed

POST /v1/payments checks its body’s size and media type itself: a body over 16,384 bytes, or one that is not application/json in UTF-8, returns 400 INVALID_REQUEST, never 413 or 415; its cancellation (POST /v1/payments/{id}/cancel) likewise answers 400 INVALID_REQUEST past its own bound, and the phone search and its export (POST /v1/payments/search/phone and POST /v1/payments/search/phone/export) answer it past 4 KiB; the export’s 413 EXPORT_TOO_LARGE means more than 5,000 matching payments, never a large body. From 0.56.0, every other /v1 POST (payment links and their cancellation, refunds, keys and webhooks) answers 413 REQUEST_TOO_LARGE to a body over 16 KiB, chunked bodies included, as the body limit of some portal and staff commands does (KYB, branding, settlement accounts, refund decisions and thresholds, notification preferences), with DOCUMENT_TOO_LARGE for a KYB document; multipart is disabled, so the framework itself never answers 413. A timed-out asynchronous answer would be 503 SERVICE_UNAVAILABLE, but no route answers asynchronously today.

On /v1, a route no scope rule covers returns 403 INSUFFICIENT_SCOPE. From 0.55.0, a client that went away is answered nothing. A request the servlet container answers itself (a firewall rejection, a failure before routing) carries only {timestamp, status, error, path}; there is no HTML error page. From 0.51.0, every authenticated /v1 response carries X-Request-Id, the ID its request log entry keeps.

A merchant can register one test endpoint with POST /v1/webhooks/endpoint. Local verification permits only 127.0.0.1: exactly http://127.0.0.1:18160/sandbox/webhooks, the channels service’s sink, or a port from 18161 to 18169 at the receiver’s own path, at most 512 characters, with an optional query of at most 512. In the hosted sandbox only that sink listens, inside Futa: deliveries never reach your server there, and the developer console lists them (webhooks in the hosted sandbox). No redirects or arbitrary network destinations are allowed. Production external endpoint registration is separate work.

The signature header is Futa-Signature: t=<seconds>,v1=<hex>. Compute HMAC-SHA256 over the exact bytes of timestamp + "." + raw_body, verify in constant time and reject old timestamps. Use the event UUID for deduplication and the per-payment sequence for ordering. Do not trust network arrival order.

POST /v1/webhooks/endpoint/rotate returns a new signing secret once. During its one-hour overlap the header includes both valid v1 signatures. Accept any matching signature for a configured current/previous secret. A further rotation replaces the overlap.

Read /v1/webhooks/deliveries and /v1/webhooks/deliveries/{id}/attempts for delivery history (webhooks:read). From 0.48.0 the list pages beyond the latest 100 with a cursor bound to your merchant, and each delivery states its state (see delivery history and replay). Replay a merchant-owned event using POST /v1/webhooks/deliveries/{id}/replay with Idempotency-Key (webhooks:manage). Replay retains event identity, preserves history and renews the 72-hour retry window. At most one new replay per event per minute is accepted.

Staff use separate Keycloak JWT credentials and roles. A maker proposes a merchant configuration change; a different checker approves it. A user with both roles still cannot approve their own action. The first successful creation/recovery-key approval reveals a test key once; repeated approvals return metadata only.

Before approving, read GET /staff/actions/{id} and inspect its request object. It contains the persisted proposal, including the merchant display name and effective request quota. The same proposal is included in creation, retry and approval responses.

These APIs exercise synthetic payments. They do not provision live participants or certify a production identity, archival or security deployment. See the platform milestone and verification reports for the current acceptance evidence.

For authenticated human users, see merchant teams: live membership, role changes, invitations and revocation use a separate verified-identity boundary.