M2M Withdrawals
On this page
The M2M Withdrawals API is the server-to-server interface of Payout API v2 for creating, retrieving and cancelling withdrawals: transfers from your Payout balance to a customer's IBAN. It is secured by two qualified certificates issued by a Qualified Trust Service Provider (QTSP) under the EU eIDAS regulation:
- QWAC (Qualified Website Authentication Certificate): establishes a mutually authenticated TLS connection (mTLS).
- QSEAL (Qualified Electronic Seal Certificate): signs each payment instruction (detached JWS).
Note
Importing, approving and renewing certificates is described in mTLS client certificates. This page covers only what is specific to withdrawals: hosts, endpoints, QSEAL signing and error behaviour.
Endpoints
| Environment | mTLS host |
|---|---|
| Sandbox | https:// |
| Production | https:// |
| Method | Path | Authentication | Purpose |
|---|---|---|---|
POST |
/ |
mTLS + QSEAL | Create a withdrawal |
GET |
/ |
mTLS | List withdrawals |
GET |
/ |
mTLS | Retrieve a withdrawal |
POST |
/ |
mTLS + QSEAL | Cancel a withdrawal that has not been processed yet |
POST |
/ |
mTLS | Check whether a withdrawal can be cancelled |
Every request also needs a bearer token from POST /api/v1/authorize on the standard host, see mTLS client certificates.
Prerequisites
Before you can call any v2 withdrawal endpoint, your account needs an approved QWAC and an approved QSEAL certificate. Both must be issued by a supported QTSP on the EU Trusted List, currently I.CA and Disig. For a certificate from another QTSP on the list, contact [email protected]. Standard eIDAS qualified certificates are enough; no PSD2-specific extensions are required (see Certificate profiles).
The full setup is in mTLS client certificates. In short:
- Get the certificates. Obtain a QWAC and a QSEAL from a QTSP.
- Import them. Import both with
POST /api/v1/mtls/certificateson the standard host (sandbox.payout.oneorapp.payout.one). - Wait for approval. Payout approves them manually.
- Call the API. Call the v2 endpoints on the mTLS host and present the QWAC in the TLS handshake.
Signing payment instructions with QSEAL
Every request that creates or changes a withdrawal (POST /api/v2/withdrawals, POST /api/v2/withdrawals/:id/cancel) must be signed with a detached JWS made with the private key of your QSEAL certificate. The other requests (GET … and cancel_allowed) need only the bearer token and the QWAC, no QSEAL signature.
Required headers
| Header | Value |
|---|---|
Authorization |
Bearer <TOKEN> |
Content-Type |
application/ |
Digest |
SHA-256=<base64(sha256(body))>, computed over the exact bytes you send |
X-JWS-Signature |
<protected-header>..<signature> (detached JWS) |
Idempotency-Key |
Optional, on create. If a withdrawal with the same key already exists for your account, it is returned with status 200 instead of creating a new one. |
JWS protected header
The protected header is a base64url-encoded JSON object:
{
"alg": "PS256",
"typ": "JOSE+JSON",
"x5t#S256": "<sha256 thumbprint of QSEAL cert, lowercase hex>",
"crit": ["sigT", "sigD"],
"sigT": "2026-05-15T10:00:00Z",
"sigD": {
"mId": "http://uri.etsi.org/19182/HttpHeaders",
"pars": ["digest"]
}
}
The signed payload is the value of the Digest header, not the body itself. Accepted algorithms: PS256, RS256 and ES256.
Signing flow (pseudocode)
body = '{"amount":"100","currency":"EUR",...}'
digest_b64 = base64( sha256(body) )
digest_header = "SHA-256=" + digest_b64
protected_b64 = base64url( JSON.stringify(protected_header) )
payload_b64 = base64url( digest_header )
signing_input = protected_b64 + "." + payload_b64
signature_b64 = base64url( sign(signing_input, qseal_private_key, "PS256") )
x_jws_signature = protected_b64 + ".." + signature_b64
Server-side verification
Payout verifies, for every QSEAL-signed request:
- The
Digestheader matches the SHA-256 of the received body. - The
x5t#S256in the protected header matches anapprovedQSEAL certificate of your account. - The certificate has not expired.
sigTis within ±5 minutes of the server time (replay protection).- The JWS signature is valid for the public key of that certificate.
If any check fails, the response is 403 Forbidden.
Example: full request
POST /api/v2/withdrawals HTTP/1.1
Host: api-mtls.payout.one
Authorization: Bearer <TOKEN>
Content-Type: application/json
Digest: SHA-256=wVuNpX91QYPeRl7fKRMbY0JVnKFicMJN9CGZ/ScU9dk=
X-JWS-Signature: <base64url protected header>..<base64url signature>
{
"amount": "10000",
"currency": "EUR",
"external_id": "merchant-tx-2026-05-15-001",
"iban": "SK3112000000198742637541",
"customer": {
"first_name": "Anna",
"last_name": "Nová",
"email": "[email protected]"
},
"statement_descriptor": "Platba 2026/05",
"nonce": "5b6e9c1a-f4a2-4c11-9e56-2f93e1c7a3d0",
"require_vop": true,
"additional_attribute": "Invoice 2026/05/017"
}
Note
The Digest above is the SHA-256 of this exact body, without a trailing newline. When the request is signed with QSEAL, the HMAC signature field of /api/v1/withdrawals is not required: the QSEAL signature protects the whole body.
Verification of Payee (VoP)
You can ask Payout to run a Verification of Payee check as part of a withdrawal. It confirms that the name you provided matches the account holder the beneficiary bank has on record for the IBAN. There is no separate VoP endpoint: you opt in per withdrawal with the optional require_vop field in the create request.
| Field | Type | Required | Description |
|---|---|---|---|
require_vop |
boolean | no | Default false. When true, Payout runs a VoP check on the withdrawal's iban and customer name and sends the result in a webhook (see below). |
additional_attribute |
string | no | Free text returned in the VoP webhook, for example an internal reference or an invoice number. |
Provide the payee's identity in the customer object, either as a natural person or as a legal entity:
| Payee | customer fields |
|---|---|
| Natural person | first_name + last_name |
| Legal entity (company) | name (company name) + company_id (organisation identifier, such as IČO or LEI) |
For a company, send name and company_id instead of first_name / last_name:
"customer": {
"name": "Example Company s. r. o.",
"company_id": "12345678",
"email": "[email protected]"
}
The VoP check does not block the withdrawal: it runs alongside it and reports its result asynchronously.
Note
VoP returns a match outcome, not an authoritative answer. A CLOSE_MATCH does not mean the payment is safe: show the returned real_name to your operator and let them confirm or cancel the withdrawal.
VoP webhook
As soon as the VoP result is known, Payout sends a webhook of type withdrawal.vop_result to your configured webhook URL. It uses the standard webhook envelope; data carries the VoP outcome and the identifiers that match it to the withdrawal:
{
"type": "withdrawal.vop_result",
"object": "webhook",
"data": {
"match_result": "CLOSE_MATCH",
"real_name": "Anna Nová-Kováčová",
"reference_id": "f1c8d4a3-2e90-4a52-9b13-7c8a1d4e5b21",
"timestamp": "2026-05-20T10:14:33Z",
"additional_attribute": "Invoice 2026/05/017",
"withdrawal_id": 90412,
"external_id": "merchant-tx-2026-05-15-001"
},
"external_id": "merchant-tx-2026-05-15-001",
"nonce": "UzhER2lFOFZCNkNQVmNuNQ",
"signature": "ab087a4c72388cc2e14fe5c2b278c4aac5b404d4f439f9d761bb660122c05f91"
}
Verify signature as for other webhooks: lowercase(hex(SHA-256("external_id|type|nonce|client_secret"))), with the top-level external_id, type and nonce of the envelope.
data fields:
| Field | Type | Description |
|---|---|---|
match_result |
enum | One of MATCH, CLOSE_MATCH, NO_MATCH, CANNOT_VERIFY. |
real_name |
string or null | Set only when match_result is CLOSE_MATCH: the name the beneficiary bank has on record, so you can show it to your operator for confirmation. |
reference_id |
UUID | ID of the check for audit purposes. Quote it in support requests. |
timestamp |
ISO 8601 | UTC datetime when the check was processed. |
additional_attribute |
string or null | The additional_attribute of the withdrawal request, if you sent one. |
withdrawal_id |
integer | Payout ID of the withdrawal the check belongs to. |
external_id |
string | Your external_id from the withdrawal request. |
Match outcomes
| Outcome | Meaning |
|---|---|
MATCH |
Full correspondence between the submitted name and the beneficiary bank's records. |
CLOSE_MATCH |
Partial correspondence (typos, missing diacritics, suffix mismatch). real_name is returned. |
NO_MATCH |
No correspondence — the name does not belong to the IBAN. |
CANNOT_VERIFY |
The check could not be completed (beneficiary bank unreachable, IBAN unknown to it, or the bank has opted out of VoP). |
Important
VoP only tells you whether the name matches the account holder of the IBAN. It is not a substitute for AML screening, sanctions list checks or any other due diligence you are required to perform on a payee.
Testing in sandbox
In sandbox the VoP outcome is determined by the payee iban, so you can exercise every branch of your withdrawal.vop_result handling. Create a withdrawal with require_vop: true to one of these IBANs:
| IBAN | match_result |
|---|---|
SK5409000000000000000001 |
MATCH |
SK2709000000000000000002 |
CLOSE_MATCH — real_name is the submitted name with first and last name swapped |
SK9709000000000000000003 |
NO_MATCH |
SK7009000000000000000004 |
CANNOT_VERIFY |
| any other IBAN | CANNOT_VERIFY |
The webhook arrives a few seconds after the 201 response.
HTTP error codes
Errors of the token and of the certificate import are described in mTLS client certificates. The withdrawal endpoints return:
| Code | Meaning |
|---|---|
200 OK |
On create: a withdrawal with the same Idempotency-Key already exists and is returned. On cancel: also when the withdrawal can no longer be cancelled; the body is then {"allowed": false, "status": "<current status>"}. |
400 Bad Request |
iban is missing, the balance is zero or too low, or the currency is invalid or not enabled for your account. |
401 Unauthorized |
The bearer token is missing, invalid or expired. |
403 Forbidden |
The QWAC or the QSEAL signature was not accepted (see Server-side verification), or the withdrawal belongs to another account. |
404 Not Found |
The withdrawal does not exist, or the request was not sent to the mTLS host (empty body). |
422 Unprocessable Entity |
Validation failed, with errors per field (for example a blocked IBAN or an invalid customer), or the risk check refused the withdrawal (Not allowed to proceed.). |
A TLS handshake is refused when no client certificate is presented or it is not issued by a trusted QTSP.
- Need help? Contact support.
- Questions? Contact sales.
- Service status? status.payout.one.
- LLM? Read llms.txt.