PayoutID API
PayoutID is Payout's OAuth2 and OpenID Connect provider and identity verification service.
- OAuth2 – redirect users to PayoutID for authorization, then exchange the result for an access token. Tokens can also be refreshed, or issued to your own client with client credentials.
- Verifications – create identity verification invitations for your customers and receive the results by webhook.
Guides: OAuth2 and identity verification.
Environments
| Environment | Base URL |
|---|---|
| Sandbox | https://id-sa.payout.one |
| Production | https://id.payout.one |
Authentication
Payout registers your application as a PayoutID client and gives you a client_id (a UUID) and a client_secret. The client also holds your registered redirect URIs and the scopes you may request.
Token endpoint
Authenticate your client at Get access token with one of these methods:
client_secret_basic– HTTP Basic authentication withclient_idas the user name andclient_secretas the passwordclient_secret_post–client_secretas a body parameterclient_secret_jwt– a JWT signed with your client secret, sent inclient_assertionprivate_key_jwt– a JWT signed with your private key, sent inclient_assertion
client_secret_basic and client_secret_post are enabled for every client by default. The JWT methods work only if Payout configured them for your client. Always send client_id in the body as well.
Verifications API
Send the access token from the token endpoint in the Authorization header:
Authorization: Bearer <access_token>
Access tokens are JWTs signed by PayoutID. A token works only in the environment that issued it: https://id-sa.payout.one for Sandbox, https://id.payout.one for Production.
Errors
How an error is reported depends on the endpoint.
Token endpoint errors
Get access token returns errors as JSON with error and error_description:
{
"error": "invalid_grant",
"error_description": "Given authorization code is invalid, revoked, or expired."
}
The HTTP status is 400, except for invalid_client (401) and unknown_error (500). Error codes:
invalid_request– a parameter is missing or malformed, or the PKCE code verifier is wronginvalid_grant– the authorization code or refresh token is invalid, revoked or expiredinvalid_scope– a requested scope is not enabled for your client or not allowed for this grantunsupported_grant_type– the grant type is not enabled for your clientinvalid_client– unknown client, wrong client secret, or aredirect_urithat does not matchunknown_error– unexpected server error
Authorization redirect errors
Authorize user reports errors by redirecting the browser to your redirect_uri with error and, when available, error_description and state. Codes you can expect:
access_denied– the user denied accessinvalid_scope– a requested scope is unknown, not enabled for your client, or cannot be combined with the other requested scopesinvalid_request– the request is invalid, for examplecode_challengeis missing although PKCE is enabled for your clientlogin_expired– the login page expired before the user logged in
If client_id or redirect_uri is invalid, PayoutID cannot redirect back safely and shows an error page to the user instead.
Verifications API errors
403– the token is missing, invalid or expired, or lacks scopeverify. The body is the plain textUNAUTHORIZED: Missing or insuficient authorization, although theContent-Typeheader saysapplication/json.422– validation failed. The body is JSON with the messages by field:{"errors": {"<field>": ["<message>"]}}.
Starts the authorization code flow. Open this URL in the user's browser: it is a page, not an API call.
Authorization flow
- PayoutID asks the user to log in or register, if needed.
- The user approves the requested scopes.
- PayoutID redirects the browser to
redirect_uriwithcodeandstate. If the user denies access or the request fails, the redirect carrieserrorinstead (see Authorization redirect errors). - Exchange
codefor tokens with Get access token andgrant_type=authorization_code. The code is valid for at most 60 seconds and works only once.
https://id-sa.payout.one/oauth/authorize?client_id=c24760a3-134f-4ff5-891b-e506a025a530&response_type=code&redirect_uri=https://www.example.com&scope=openid%20profile&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&state=c0416c89-02fb-4c64-b4f6-9ebee510f4a0Parameters
Your client ID.
Selects the authorization code flow.
Where to send the user back. Must match a redirect URI registered for your client.
Space-separated scopes to authorize. Each scope must be enabled for your client. Include openid to also get an id_token.
PKCE code challenge, derived from your code_verifier as set by code_challenge_method (RFC 7636). Required when PKCE is enabled for your client.
How code_challenge is derived from code_verifier. Use S256.
S256code_challengeis the unpadded BASE64URL encoding of the SHA-256 hash ofcode_verifierplaincode_challengeiscode_verifieritself
Returned unchanged in the redirect to redirect_uri, so you can match the response to your request.
Responses
Redirects the browser to the PayoutID login and consent pages, and finally back to redirect_uri.
Issues an access token to your client. grant_type selects what you exchange for it and which other parameters you send.
Authenticate your client as described in Authentication. To call Create invitation, use client_credentials with scope verify.
The body can be application/x-www-form-urlencoded or JSON.
curl -X POST 'https://id-sa.payout.one/oauth/token' \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"client_id": "c24760a3-134f-4ff5-891b-e506a025a530",
"code": "63a74bf4-fa3d-4b21-8350-8c51ae47d74a",
"redirect_uri": "https://www.example.com",
"code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
}'const res = await fetch("https://id-sa.payout.one/oauth/token", {
method: "POST",
headers: {
Authorization: "Basic " + Buffer.from(`${process.env.CLIENT_ID}:${process.env.CLIENT_SECRET}`).toString("base64"),
"Content-Type": "application/json",
},
body: JSON.stringify({
"grant_type": "authorization_code",
"client_id": "c24760a3-134f-4ff5-891b-e506a025a530",
"code": "63a74bf4-fa3d-4b21-8350-8c51ae47d74a",
"redirect_uri": "https://www.example.com",
"code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
}),
});
const data = await res.json();import os, requests
res = requests.post(
"https://id-sa.payout.one/oauth/token",
auth=(os.environ["CLIENT_ID"], os.environ["CLIENT_SECRET"]),
json={
"grant_type": "authorization_code",
"client_id": "c24760a3-134f-4ff5-891b-e506a025a530",
"code": "63a74bf4-fa3d-4b21-8350-8c51ae47d74a",
"redirect_uri": "https://www.example.com",
"code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
},
)
data = res.json()<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://id-sa.payout.one/oauth/token");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_USERPWD, getenv("CLIENT_ID") . ":" . getenv("CLIENT_SECRET"));
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"grant_type" => "authorization_code",
"client_id" => "c24760a3-134f-4ff5-891b-e506a025a530",
"code" => "63a74bf4-fa3d-4b21-8350-8c51ae47d74a",
"redirect_uri" => "https://www.example.com",
"code_verifier" => "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCJ9.eyJpc3MiOiJodHRwczovL2lkLXNhLnBheW91dC5vbmUifQ.…",
"token_type": "bearer",
"expires_in": 86400,
"refresh_token": "4b217f27-836a-4a12-97a3-b962c23dedc5"
}Request body
What you exchange for the token.
authorization_codeExchange thecodefrom Authorize user. Sendcodeandredirect_uri, pluscode_verifierwhen PKCE is enabled for your client. Client authentication is required for a confidential client.refresh_tokenGet a new access token withrefresh_token, optionally for fewer scopes withscope. Client authentication is required unless public refresh is enabled for your client.client_credentialsGet a token for your own client, without a user, optionally withscope. Client authentication is required.
Your client ID. Send it in the body even when you authenticate with HTTP Basic.
Your client secret, for client_secret_post. Omit it when you authenticate with HTTP Basic.
The code from the authorization redirect. Required for authorization_code.
The same redirect_uri as in the authorization request. Required for authorization_code.
PKCE code verifier. Required for authorization_code when PKCE is enabled for your client.
Refresh token from an earlier token response. Required for refresh_token. It works only once; the response carries a new one.
Space-separated scopes. With client_credentials, each must be enabled for your client, and PISPSUBMIT is not allowed. With refresh_token, at most the scopes of the original token, which are the default.
Only for clients configured for client_secret_jwt or private_key_jwt.
Signed JWT, only for clients configured for client_secret_jwt or private_key_jwt. Set sub to your client ID and aud to the PayoutID URL of the environment. The iss and exp claims are required too.
Response 200
Access token (JWT). Send it as Authorization: Bearer <access_token>.
Token type.
Seconds until the access token expires.
Refresh token for the refresh_token grant.
OpenID Connect ID token (JWT). Returned by the authorization_code grant when the openid scope was granted.
Other responses
The request, grant or scope is invalid, or the grant type is not enabled for your client. See Token endpoint errors.
Example
{
"error": "invalid_grant",
"error_description": "Given authorization code is invalid, revoked, or expired."
}Client authentication failed (invalid_client).
Example
{
"error": "invalid_client",
"error_description": "Invalid client_id or client_secret."
}id-sa.payout.one · id.payout.oneCreates an identity verification invitation for one of your customers and returns the redirect_url to send them to.
Authenticate with a client_credentials access token that has scope verify (see Get access token).
Verification flow
- Create the invitation and redirect the customer to
redirect_url. - The customer completes the verification steps and is redirected to
callback_url. - When all data are collected, PayoutID sends the customer's details by webhook to
notify_url, and the AML check results toaml_notify_url.
The identity verification guide describes the webhooks and how to verify their signatures.
curl -X POST 'https://id-sa.payout.one/api/v1/verification/invitation' \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"client_provided_iban": "CZ6508000000192000145399",
"bank_account_requested": true,
"callback_url": "https://example.com",
"notify_url": "https://example.com/webhooks/payout-id"
}'const res = await fetch("https://id-sa.payout.one/api/v1/verification/invitation", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PAYOUT_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"client_provided_iban": "CZ6508000000192000145399",
"bank_account_requested": true,
"callback_url": "https://example.com",
"notify_url": "https://example.com/webhooks/payout-id"
}),
});
const data = await res.json();import os, requests
res = requests.post(
"https://id-sa.payout.one/api/v1/verification/invitation",
headers={"Authorization": f"Bearer {os.environ['PAYOUT_TOKEN']}"},
json={
"client_provided_iban": "CZ6508000000192000145399",
"bank_account_requested": True,
"callback_url": "https://example.com",
"notify_url": "https://example.com/webhooks/payout-id",
},
)
data = res.json()<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://id-sa.payout.one/api/v1/verification/invitation");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"client_provided_iban" => "CZ6508000000192000145399",
"bank_account_requested" => true,
"callback_url" => "https://example.com",
"notify_url" => "https://example.com/webhooks/payout-id"
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer " . getenv("PAYOUT_TOKEN"), "Content-Type: application/json"]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);{
"id": "f2a060df-4812-4335-8706-1c29e61c8b47",
"redirect_url": "https://id-sa.payout.one/verifications/f2a060df-4812-4335-8706-1c29e61c8b47"
}Request body
Customer email, prefilled in the verification.
Customer first name, prefilled in the verification.
Customer last name, prefilled in the verification.
Include the customer's bank account details in the webhook. Requires scope account_info.
Run an AML check on the customer. Requires scope aml and aml_notify_url.
The IBAN the customer must verify, when you need that specific account rather than any account the customer can access. Must be a valid IBAN of a supported Slovak or Czech bank.
Where the customer is redirected after completing the verification steps.
Receives the webhook with the customer's details when all data are collected.
Receives the webhook with the AML check results. Required when aml_requested is true.
Response 200
Invitation ID. Both webhooks carry it as data.id.
Verification page to redirect the customer to.
Other responses
The access token is missing, invalid or expired, or lacks scope verify. The body is plain text, although the Content-Type header says application/json.
Example
UNAUTHORIZED: Missing or insuficient authorizationValidation failed. errors holds the messages by field, for example missing scope for condition when bank_account_requested or aml_requested is set without its scope.
Example
{
"errors": {
"client_provided_iban": [
"invalid IBAN checksum"
]
}
}- Need help? Contact support.
- Questions? Contact sales.
- Service status? status.payout.one.
- LLM? Read llms.txt.