# 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](https://developers.payout.tech/guides/payout-id-oauth.html) and [identity verification](https://developers.payout.tech/guides/payout-id-identity-verification.html).

## Environments

- 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](#endpoint_to_retrieve_authorization_token) with one of these methods:

- `client_secret_basic` – HTTP Basic authentication with `client_id` as the user name and `client_secret` as the password
- `client_secret_post` – `client_secret` as a body parameter
- `client_secret_jwt` – a JWT signed with your client secret, sent in `client_assertion`
- `private_key_jwt` – a JWT signed with your private key, sent in `client_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:

```http
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](#endpoint_to_retrieve_authorization_token) returns errors as JSON with `error` and `error_description`:

```json
{
  "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 wrong
- `invalid_grant` – the authorization code or refresh token is invalid, revoked or expired
- `invalid_scope` – a requested scope is not enabled for your client or not allowed for this grant
- `unsupported_grant_type` – the grant type is not enabled for your client
- `invalid_client` – unknown client, wrong client secret, or a `redirect_uri` that does not match
- `unknown_error` – unexpected server error

### Authorization redirect errors

[Authorize user](#authorization_redirect_for_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 access
- `invalid_scope` – a requested scope is unknown, not enabled for your client, or cannot be combined with the other requested scopes
- `invalid_request` – the request is invalid, for example `code_challenge` is missing although PKCE is enabled for your client
- `login_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 scope `verify`. The body is the plain text `UNAUTHORIZED: Missing or insuficient authorization`, although the `Content-Type` header says `application/json`.
- `422` – validation failed. The body is JSON with the messages by field: `{"errors": {"<field>": ["<message>"]}}`.


## Authorize user

`GET https://id-sa.payout.one/oauth/authorize`

Starts the authorization code flow. Open this URL in the user's browser: it is a page, not an API call.

### Authorization flow

1. PayoutID asks the user to log in or register, if needed.
2. The user approves the requested scopes.
3. PayoutID redirects the browser to `redirect_uri` with `code` and `state`. If the user denies access or the request fails, the redirect carries `error` instead (see [Authorization redirect errors](#authorization-redirect-errors)).
4. Exchange `code` for tokens with [Get access token](#endpoint_to_retrieve_authorization_token) and `grant_type=authorization_code`. The code is valid for at most 60 seconds and works only once.

### Parameters

- `client_id` (query, required) — Your client ID.
- `response_type` (query, required) — Selects the authorization code flow.
  - `code`
- `redirect_uri` (query, required) — Where to send the user back. Must match a redirect URI registered for your client.
- `scope` (query) — Space-separated scopes to authorize. Each scope must be enabled for your client. Include `openid` to also get an `id_token`.
- `code_challenge` (query) — PKCE code challenge, derived from your `code_verifier` as set by `code_challenge_method` ([RFC 7636](https://www.rfc-editor.org/rfc/rfc7636)). Required when PKCE is enabled for your client.
- `code_challenge_method` (query) — How `code_challenge` is derived from `code_verifier`. Use `S256` (default `plain`)
  - `S256` — `code_challenge` is the unpadded BASE64URL encoding of the SHA-256 hash of `code_verifier`
  - `plain` — `code_challenge` is `code_verifier` itself
- `state` (query) — Returned unchanged in the redirect to `redirect_uri`, so you can match the response to your request.

### Responses

- `302` — Redirects the browser to the PayoutID login and consent pages, and finally back to `redirect_uri`.

### Redirect URL

```text
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-9ebee510f4a0
```


## Get access token

`POST https://id-sa.payout.one/oauth/token`

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](#authentication). To call [Create invitation](#create_invitation), use `client_credentials` with scope `verify`.

The body can be `application/x-www-form-urlencoded` or JSON.

### Request body

- `grant_type` — string (required) · What you exchange for the token.
  - `authorization_code` — Exchange the `code` from [Authorize user](#authorization_redirect_for_user). Send `code` and `redirect_uri`, plus `code_verifier` when PKCE is enabled for your client. Client authentication is required for a confidential client.
  - `refresh_token` — Get a new access token with `refresh_token`, optionally for fewer scopes with `scope`. Client authentication is required unless public refresh is enabled for your client.
  - `client_credentials` — Get a token for your own client, without a user, optionally with `scope`. Client authentication is required.
- `client_id` — string<uuid> (required) · Your client ID. Send it in the body even when you authenticate with HTTP Basic. · e.g. `c24760a3-134f-4ff5-891b-e506a025a530`
- `client_secret` — string · Your client secret, for `client_secret_post`. Omit it when you authenticate with HTTP Basic.
- `code` — string · The `code` from the authorization redirect. Required for `authorization_code`. · e.g. `63a74bf4-fa3d-4b21-8350-8c51ae47d74a`
- `redirect_uri` — string · The same `redirect_uri` as in the authorization request. Required for `authorization_code`. · e.g. `https://www.example.com`
- `code_verifier` — string · PKCE code verifier. Required for `authorization_code` when PKCE is enabled for your client. · e.g. `dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk`
- `refresh_token` — string · Refresh token from an earlier token response. Required for `refresh_token`. It works only once; the response carries a new one. · e.g. `4b217f27-836a-4a12-97a3-b962c23dedc5`
- `scope` — string · 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. · e.g. `verify account_info`
- `client_assertion_type` — string · Only for clients configured for `client_secret_jwt` or `private_key_jwt`. · one of `urn:ietf:params:oauth:client-assertion-type:jwt-bearer`
- `client_assertion` — string · 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` — string (required) · Access token (JWT). Send it as `Authorization: Bearer <access_token>`.
- `token_type` — string (required) · Token type. · one of `bearer`
- `expires_in` — integer (required) · Seconds until the access token expires. · e.g. `86400`
- `refresh_token` — string · Refresh token for the `refresh_token` grant.
- `id_token` — string · OpenID Connect ID token (JWT). Returned by the `authorization_code` grant when the `openid` scope was granted.

### Responses

- `200` — Success
- `400` — The request, grant or scope is invalid, or the grant type is not enabled for your client. See [Token endpoint errors](#token-endpoint-errors).
- `401` — Client authentication failed (`invalid_client`).

### Example

```bash
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"
     }'
```


## Create invitation

`POST https://id-sa.payout.one/api/v1/verification/invitation`

Hosts: `https://id-sa.payout.one/api`, `https://id.payout.one/api`

Creates 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](#endpoint_to_retrieve_authorization_token)).

### Verification flow

1. Create the invitation and redirect the customer to `redirect_url`.
2. The customer completes the verification steps and is redirected to `callback_url`.
3. When all data are collected, PayoutID sends the customer's details by webhook to `notify_url`, and the AML check results to `aml_notify_url`.

The [identity verification guide](https://developers.payout.tech/guides/payout-id-identity-verification.html) describes the webhooks and how to verify their signatures.

### Request body

- `provided_email` — string<email> · Customer email, prefilled in the verification. · e.g. `john.doe@example.com`
- `provided_name` — string · Customer first name, prefilled in the verification. · e.g. `John`
- `provided_surname` — string · Customer last name, prefilled in the verification. · e.g. `Doe`
- `bank_account_requested` — boolean · Include the customer's bank account details in the webhook. Requires scope `account_info`.
- `aml_requested` — boolean · Run an AML check on the customer. Requires scope `aml` and `aml_notify_url`.
- `client_provided_iban` — string · 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. · e.g. `CZ6508000000192000145399`
- `callback_url` — string (required) · Where the customer is redirected after completing the verification steps. · e.g. `https://example.com`
- `notify_url` — string (required) · Receives the webhook with the customer's details when all data are collected. · e.g. `https://example.com/webhooks/payout-id`
- `aml_notify_url` — string · Receives the webhook with the AML check results. Required when `aml_requested` is `true`. · e.g. `https://example.com/webhooks/payout-id-aml`

### Response 200

- `id` — string<uuid> (required) · Invitation ID. Both webhooks carry it as `data.id`.
- `redirect_url` — string (required) · Verification page to redirect the customer to.

### Responses

- `200` — Success
- `403` — 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`.
- `422` — Validation 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

```bash
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"
     }'
```

