# Quickstart (/docs/getting-started)
This quickstart walks through the three API calls every integration makes:
exchange your client secret for a JWT, create a payment, and handle the
webhook that tells you whether the customer paid.
## Prerequisites [#prerequisites]
* A verified merchant account ([sign up](/docs/saas-dashboard/getting-started))
* At least one bank destination configured in the dashboard
* An API key with a webhook URL — create one at **Open Finance → API Keys**
Your client secret is shown **once** at creation time. Store it in an
environment variable or a secrets manager on your backend. Never put it in
client-side code, browser storage, or a public repository. If it leaks,
revoke the key immediately.
## 1. Get an access token [#1-get-an-access-token]
Exchange your client secret for a short-lived JWT. Tokens are valid for
60 minutes.
```bash
curl -X POST https://payment-api.openfinance.aryze.io/v1/auth/token \
-H 'Content-Type: application/json' \
-d '{
"grant_type": "client_credentials",
"client_secret": "'"$ARYZE_CLIENT_SECRET"'"
}'
```
```ts
const res = await fetch(
'https://payment-api.openfinance.aryze.io/v1/auth/token',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_secret: process.env.ARYZE_CLIENT_SECRET,
}),
},
);
if (!res.ok) throw new Error(`Auth failed: ${res.status}`);
const { access_token } = await res.json();
```
```python
import os
import requests
res = requests.post(
'https://payment-api.openfinance.aryze.io/v1/auth/token',
json={
'grant_type': 'client_credentials',
'client_secret': os.environ['ARYZE_CLIENT_SECRET'],
},
)
res.raise_for_status()
access_token = res.json()['access_token']
```
```csharp
using System.Net.Http.Json;
var http = new HttpClient();
var res = await http.PostAsJsonAsync(
"https://payment-api.openfinance.aryze.io/v1/auth/token",
new {
grant_type = "client_credentials",
client_secret = Environment.GetEnvironmentVariable("ARYZE_CLIENT_SECRET"),
});
res.EnsureSuccessStatusCode();
var body = await res.Content.ReadFromJsonAsync();
var accessToken = body!.access_token;
record TokenResponse(string access_token, string token_type, int expires_in, string environment);
```
The response contains the token you'll pass as `Authorization: Bearer `
on every subsequent call.
```json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"environment": "sandbox"
}
```
## 2. Initiate a payment [#2-initiate-a-payment]
Create a payment session. The destination bank account is read from the API
key you authenticated with, so you don't pass it in the request.
```bash
curl -X POST https://payment-api.openfinance.aryze.io/v1/payments/initiate \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"redirectUrl": "https://yourapp.com/checkout/return",
"amount": 100.50,
"currencyCode": "EUR",
"reference": "ORDER12345"
}'
```
```ts
const res = await fetch(
'https://payment-api.openfinance.aryze.io/v1/payments/initiate',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${access_token}`,
},
body: JSON.stringify({
redirectUrl: 'https://yourapp.com/checkout/return',
amount: 100.5,
currencyCode: 'EUR',
reference: 'ORDER12345',
}),
},
);
const payment = await res.json();
// Redirect the customer to complete the payment:
// window.location.href = payment.flowUrl;
```
```python
res = requests.post(
'https://payment-api.openfinance.aryze.io/v1/payments/initiate',
headers={'Authorization': f'Bearer {access_token}'},
json={
'redirectUrl': 'https://yourapp.com/checkout/return',
'amount': 100.50,
'currencyCode': 'EUR',
'reference': 'ORDER12345',
},
)
res.raise_for_status()
payment = res.json()
# Redirect the customer to payment['flowUrl']
```
```csharp
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
var res = await http.PostAsJsonAsync(
"https://payment-api.openfinance.aryze.io/v1/payments/initiate",
new {
redirectUrl = "https://yourapp.com/checkout/return",
amount = 100.50m,
currencyCode = "EUR",
reference = "ORDER12345",
});
res.EnsureSuccessStatusCode();
var payment = await res.Content.ReadFromJsonAsync();
```
The response gives you a `flowUrl` — redirect the customer there. They'll
choose their bank, authenticate, and authorise the transfer.
```json
{
"transactionId": "2f1a3e8c-9b7d-4a11-8c6f-d3e5e9b1a2c0",
"mastercardPaymentId": "mcob_8f3c2a...",
"flowUrl": "https://payment.aryze.io/complete/abc123...",
"status": "PENDING",
"amount": 100.50,
"currencyCode": "EUR",
"reference": "ORDER12345",
"endToEndId": "E2E-ORDER12345",
"initiationTimestamp": "2026-04-21T14:30:00Z"
}
```
## 3. Handle the webhook [#3-handle-the-webhook]
When the payment status changes, ARYZE POSTs a `payment.status_updated` event
to the webhook URL on your API key. Always respond with `200 OK`, then do any
slow work asynchronously.
```ts title="Express.js"
import express from 'express';
const app = express();
app.use(express.json());
app.post('/webhooks/aryze', async (req, res) => {
const { event, transactionId, status, reference } = req.body;
// Acknowledge immediately, then process.
res.status(200).json({ received: true });
if (event === 'payment.status_updated' && status === 'COMPLETED') {
await markOrderPaid(reference, transactionId);
}
});
app.listen(3000);
```
Webhook retries use fixed backoff: 3 attempts at 1s, 5s, and 15s. Make your
handler **idempotent** — the same event may arrive more than once.
## Next steps [#next-steps]
# ARYZE Open Finance (/docs)
ARYZE Open Finance lets you accept bank-to-bank payments from your customers
via Mastercard Open Banking. You get a single REST API, real-time webhooks for
payment status, and a dashboard to manage merchants, bank destinations, API
keys, and transactions.
Pick a starting point:
## Base URL [#base-url]
All Payment API endpoints are served from a single base URL and versioned under
`/v1`:
## Before you integrate [#before-you-integrate]
1. Sign up and verify your organization in the [Open Finance Payments Dashboard](/docs/saas-dashboard/getting-started).
2. Submit KYB so your account can issue live API keys.
3. Add at least one bank destination to receive funds.
4. Create an API key with a webhook URL — both [Payment API auth](/docs/api-reference/payment-api/create-access-token) and webhook delivery rely on it.
# API reference (/docs/api-reference)
These pages are generated from the [OpenAPI spec](https://github.com/aryze-fintech/aryze_open_finance_docs/blob/main/content/openapi/payment-api.json)
committed to this repo. Every endpoint renders a live request playground —
paste a Bearer token, fill in the body, and fire the call straight at the
production API.
## Base URL [#base-url]
All endpoints are versioned under `/v1`.
## Authentication [#authentication]
Every request except `POST /v1/auth/token` requires a Bearer JWT in the
`Authorization` header. See
[Create access token](/docs/api-reference/payment-api/create-access-token).
# Accept a payment (/docs/guides/accept-a-payment)
This guide shows the full happy path for a single payment: your server calls
`/v1/payments/initiate`, the customer completes the bank transfer through the
ARYZE-hosted flow, and your return URL receives them when they're done.
The customer's bank does the actual authentication and authorisation. Your
server never sees bank credentials — you only receive a transaction status.
## 1. Set up credentials on your backend [#1-set-up-credentials-on-your-backend]
All Payment API calls must come from your server. The client secret never
reaches the browser.
```bash title=".env"
ARYZE_CLIENT_SECRET=cs_live_...
ARYZE_API_BASE=https://payment-api.openfinance.aryze.io
```
## 2. Create a payment when the customer checks out [#2-create-a-payment-when-the-customer-checks-out]
### Get an access token [#get-an-access-token]
Tokens are valid for 60 minutes. Cache the token on your backend and reuse it
until it's close to expiry — don't request a fresh one on every payment.
```ts
async function getAccessToken(): Promise {
const res = await fetch(`${process.env.ARYZE_API_BASE}/v1/auth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_secret: process.env.ARYZE_CLIENT_SECRET,
}),
});
if (!res.ok) throw new Error(`Auth failed: ${res.status}`);
const { access_token } = await res.json();
return access_token;
}
```
### Initiate the payment [#initiate-the-payment]
Pass a unique `reference` (your internal order ID) and the URL you want the
customer returned to. Amounts are decimal numbers in the payment currency —
`100.50` means 100 euros and 50 cents, not 10050 minor units.
```ts
async function createPayment(orderId: string, amount: number) {
const token = await getAccessToken();
const res = await fetch(
`${process.env.ARYZE_API_BASE}/v1/payments/initiate`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
redirectUrl: `https://yourapp.com/checkout/return?order=${orderId}`,
amount,
currencyCode: 'EUR',
reference: orderId,
metadata: JSON.stringify({ orderId, source: 'web-checkout' }),
}),
},
);
if (!res.ok) {
const { error } = await res.json().catch(() => ({ error: res.statusText }));
throw new Error(`Payment initiation failed: ${error}`);
}
return res.json();
}
```
### Redirect to the flow URL [#redirect-to-the-flow-url]
The response contains a `flowUrl` — the hosted page where the customer picks
their bank and approves the transfer. Expose your own checkout route on your
backend, return `flowUrl` to your frontend, and redirect. `/api/checkout` here
is *your* server route, not an ARYZE endpoint.
```ts title="Your backend (e.g. Express)"
app.post('/api/checkout', async (req, res) => {
const payment = await createPayment(req.body.orderId, req.body.amount);
res.json({ flowUrl: payment.flowUrl, transactionId: payment.transactionId });
});
```
```ts title="Your frontend"
const { flowUrl } = await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({ orderId, amount }),
headers: { 'Content-Type': 'application/json' },
}).then((r) => r.json());
window.location.href = flowUrl;
```
## 3. Handle the return URL [#3-handle-the-return-url]
After the customer finishes in their banking app, they're redirected to the
`redirectUrl` you provided. **Do not trust the return URL alone** to mark the
order as paid — the authoritative signal is the webhook. Use the return URL
only to render a "thanks" page and poll for status.
```ts title="app/checkout/return/page.tsx"
export default async function ReturnPage({
searchParams,
}: {
searchParams: { order?: string };
}) {
const order = searchParams.order;
const status = await getOrderStatus(order); // your DB, updated by webhook
if (status === 'paid') return ;
if (status === 'failed') return ;
return ; // polls every few seconds
}
```
## 4. Finalise the order from the webhook [#4-finalise-the-order-from-the-webhook]
The webhook handler is where you actually mark the order as paid. See
[Consume webhooks](/docs/guides/consume-webhooks) for retry semantics and
idempotency.
## Field rules to know [#field-rules-to-know]
Pick a `reference` that is unique per order **after sanitisation** — if two
different order IDs collapse to the same 20-character alphanumeric string, you
won't be able to tell them apart in your bank statement.
# Check payment status (/docs/guides/check-payment-status)
Webhooks are the primary way to learn about status changes. Polling
`GET /v1/payments/{transactionId}` is useful for:
* A safety net in case a webhook is delayed or lost.
* Back-office tools that inspect a single transaction on demand.
* Reconciliation jobs that verify your local state against ARYZE.
Don't poll in a tight loop. Bank settlement can take seconds to minutes — use
at least a 5-second interval, stop as soon as the status is terminal
(`COMPLETED` or `FAILED`), and cap the total duration (for example, 10
minutes).
## Fetch the transaction [#fetch-the-transaction]
```bash
curl https://payment-api.openfinance.aryze.io/v1/payments/2f1a3e8c-9b7d-4a11-8c6f-d3e5e9b1a2c0 \
-H "Authorization: Bearer $ACCESS_TOKEN"
```
```ts
async function getPayment(transactionId: string, token: string) {
const res = await fetch(
`${process.env.ARYZE_API_BASE}/v1/payments/${transactionId}`,
{ headers: { Authorization: `Bearer ${token}` } },
);
if (res.status === 404) return null;
if (!res.ok) throw new Error(`Lookup failed: ${res.status}`);
return res.json();
}
```
```python
def get_payment(transaction_id: str, token: str):
res = requests.get(
f'https://payment-api.openfinance.aryze.io/v1/payments/{transaction_id}',
headers={'Authorization': f'Bearer {token}'},
)
if res.status_code == 404:
return None
res.raise_for_status()
return res.json()
```
```csharp
public async Task GetPaymentAsync(string transactionId, string token)
{
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var res = await http.GetAsync(
$"https://payment-api.openfinance.aryze.io/v1/payments/{transactionId}");
if (res.StatusCode == HttpStatusCode.NotFound) return null;
res.EnsureSuccessStatusCode();
return await res.Content.ReadFromJsonAsync();
}
```
## Status values [#status-values]
You may have seen `CANCELLED` in the dashboard's status filter — that's a UI
option, but no transaction ever sits in `CANCELLED` at the API level. A
cancelled bank transfer surfaces as `status: "FAILED"` with
`mastercardStatus: "CANCELLED"`.
## Polling pattern [#polling-pattern]
```ts
async function waitForTerminalStatus(
transactionId: string,
token: string,
{
intervalMs = 5000,
timeoutMs = 10 * 60 * 1000,
}: { intervalMs?: number; timeoutMs?: number } = {},
) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const payment = await getPayment(transactionId, token);
if (!payment) return { status: 'NOT_FOUND' } as const;
if (payment.status === 'COMPLETED' || payment.status === 'FAILED') {
return payment;
}
await new Promise((r) => setTimeout(r, intervalMs));
}
return { status: 'TIMEOUT' } as const;
}
```
## Response [#response]
The transaction response includes both the ARYZE status and the underlying
Mastercard status, plus any metadata you stored at initiation.
```json
{
"transactionId": "2f1a3e8c-9b7d-4a11-8c6f-d3e5e9b1a2c0",
"mastercardPaymentId": "mcob_8f3c2a...",
"amount": 100.50,
"currencyCode": "EUR",
"status": "COMPLETED",
"mastercardStatus": "PAYMENT_EXECUTED_CREDITED",
"paymentRail": "SepaCreditTransfer",
"reference": "ORDER12345",
"endToEndId": "E2E-ORDER12345",
"redirectUrl": "https://yourapp.com/checkout/return",
"flowUrl": "https://payment.aryze.io/complete/abc123...",
"providerId": "bank_xyz",
"metadata": "{\"orderId\":\"ORDER12345\"}",
"initiationTimestamp": "2026-04-21T14:30:00Z",
"updatedTimestamp": "2026-04-21T14:32:10Z",
"completedTimestamp": "2026-04-21T14:32:10Z"
}
```
See the [full field reference](/docs/api-reference/payment-api/get-payment).
# Consume webhooks (/docs/guides/consume-webhooks)
Whenever a payment's status changes, ARYZE POSTs a JSON body to the webhook URL
configured on your API key. There is one event type today:
`payment.status_updated`.
## Configure your webhook URL [#configure-your-webhook-url]
Webhook URLs are set per API key in the dashboard at
**Open Finance → API Keys**. You can update the URL without rotating the key.
The endpoint must be HTTPS and reachable from the public internet — non-HTTPS
URLs are rejected on save.
## Event shape [#event-shape]
```json
{
"event": "payment.status_updated",
"transactionId": "2f1a3e8c-9b7d-4a11-8c6f-d3e5e9b1a2c0",
"mastercardPaymentId": "mcob_8f3c2a...",
"status": "COMPLETED",
"amount": 100.50,
"currencyCode": "EUR",
"timestamp": "2026-04-21T14:32:10Z"
}
```
Your merchant reference and any metadata you sent on `POST /v1/payments/initiate`
are **not** included in the webhook. Look them up by `transactionId` against
your own store — you should have saved them when you created the payment.
## Acknowledge quickly, process later [#acknowledge-quickly-process-later]
Return `200 OK` as soon as you've persisted the event. Do the real work
(fulfilment, emails, accounting) in a background job. ARYZE treats any non-2xx
response as failure and retries.
```ts title="Express.js"
import express from 'express';
const app = express();
app.use(express.json());
app.post('/webhooks/aryze', async (req, res) => {
const event = req.body;
try {
await saveEventIfNew(event); // idempotency check
res.status(200).json({ received: true });
} catch (err) {
console.error('Webhook persistence failed', err);
res.status(500).json({ error: 'retry please' });
return;
}
// Off the request path — don't await this.
enqueueFulfilmentJob(event).catch(console.error);
});
app.listen(3000);
```
## Idempotency [#idempotency]
ARYZE may deliver the same event more than once (retries, network retransmits,
or the same transaction moving through the same terminal state). Store each
event by `transactionId` + `status` and skip work you've already done.
```ts
async function saveEventIfNew(event: WebhookEvent) {
const key = `${event.transactionId}:${event.status}`;
const inserted = await db.webhookEvents.insert({
key,
payload: event,
receivedAt: new Date(),
}).onConflict('key').ignore();
if (!inserted) throw new DuplicateEvent(key);
}
```
## Retry policy [#retry-policy]
If your endpoint doesn't return a 2xx response within 10 seconds, ARYZE retries.
There is **no HMAC signature** on webhook requests today. Until signing is
available, use an unguessable webhook URL (for example, include a long random
segment) and cross-check every event by fetching the transaction with
`GET /v1/payments/{transactionId}` before acting on it.
## Handling each status [#handling-each-status]
The bank has registered the payment but the customer has not yet completed
authentication and authorisation. Show a "redirecting" or "waiting" UI. Do
not fulfil the order.
This is typically the first webhook you receive for a transaction.
The bank is processing the transfer (preparing, authorising, or settling).
Show a "processing" UI. Do not fulfil the order.
You may receive `PENDING` more than once as the bank reports intermediate
states upstream — ARYZE collapses several Mastercard codes
(`PREPARING`, `AUTHORIZING`, `PROVIDER_PROCESSING`, `PENDING`) into the same
transaction status.
Funds have been credited. Fulfil the order.
If you've already fulfilled this transaction from an earlier event, skip —
the event can arrive more than once.
The transfer was cancelled, rejected, or expired upstream. Do not fulfil the
order. Mark it as failed and prompt the customer to retry if that makes sense
for your flow.
Mastercard `CANCELLED` and `REJECTED` codes both arrive here as `FAILED` —
inspect the **Mastercard status** on the transaction (via
[`GET /v1/payments/{transactionId}`](/docs/api-reference/payment-api/get-payment))
if you need the upstream reason.
# Integration Guides (/docs/guides)
Each guide solves one problem end-to-end with runnable code.
# API keys (/docs/saas-dashboard/api-keys)
An API key is the credential your backend uses to call the Payment API. Each
key is bound to one environment (sandbox or live), one bank destination, and
one webhook URL.
## Create a key [#create-a-key]
### Open the API keys page [#open-the-api-keys-page]
Go to **Open Finance → API Keys**. Make sure the environment selector in the
header matches where you want the key to run — sandbox keys can't be promoted
to live later.
### Click "Create API key" [#click-create-api-key]
Fill in:
* **Name** — how the key shows up in the dashboard (e.g. "web-checkout prod").
* **Description** — optional notes for teammates.
* **Destination** — which bank account will receive payments created with
this key.
* **Webhook URL** — where ARYZE POSTs `payment.status_updated` events. Must
be HTTPS.
* **Expiration** — either "never expires" or a specific date.
### Save the client secret [#save-the-client-secret]
The client secret is shown **once** immediately after creation. Copy it into
your secrets manager or environment variable store before closing the modal.
If you miss the secret, you have to delete the key and create a new one.
There is no way to recover a client secret later.
### Authenticate from your backend [#authenticate-from-your-backend]
Send the client secret to [`POST /v1/auth/token`](/docs/api-reference/payment-api/create-access-token)
to get a short-lived JWT, then use that JWT for payment calls.
## Activation and KYB [#activation-and-kyb]
* **Sandbox keys** are active immediately.
* **Live keys** are created as *inactive* until KYB is approved. Once
approval lands, the key flips to active on its own — no action needed.
## Update a key [#update-a-key]
For an existing key you can:
* **Change the webhook URL** — takes effect on the next event.
* **Change the description or expiration**.
* **Rotate the webhook URL** without rotating the secret — useful for
migrating webhook handlers.
You **cannot** change the destination on a live key. Create a new key bound
to the new destination, migrate your integration, then revoke the old one.
## Rotate or revoke a key [#rotate-or-revoke-a-key]
Two separate operations:
* **Rotate** — generate a new client secret for the same key. The old
secret keeps working until you explicitly expire it, so you can roll it out
without downtime.
* **Revoke** — immediately disable the key. Any subsequent
`POST /v1/auth/token` with its secret fails with `401 invalid_client`. In-flight
payments complete normally.
If a secret leaks, **revoke** first and create a new key with a new secret.
Don't rely on rotation alone — a leaked secret can obtain tokens until it's
explicitly revoked.
## Webhook URL requirements [#webhook-url-requirements]
* HTTPS endpoint.
* Reachable from the public internet.
* Responds with any 2xx status within 10 seconds.
* Unguessable path recommended (treat the URL as shared secret until HMAC
signing ships).
# Bank destinations (/docs/saas-dashboard/bank-destinations)
A **destination** is a bank account that receives funds from customer
payments. Each API key is bound to exactly one destination — which means the
destination determines the currency and payment rail used for every payment
created with that key.
## Add a destination [#add-a-destination]
### Open the bank destinations page [#open-the-bank-destinations-page]
Go to **Open Finance → Banks**. You'll see a list of your existing
destinations (empty if this is your first one) and a **Add destination**
button.
### Pick a country [#pick-a-country]
Select the country the bank account is held in. The payment rail and currency
are inferred from your choice — for example, the United Kingdom implies
`UkFasterPayments` + GBP, and Eurozone countries imply `SepaCreditTransfer` +
EUR. The dashboard shows which rail/currency pair will apply before you
confirm.
The list of available countries depends on where your business is registered
and your banking partner coverage.
### Enter account details [#enter-account-details]
Provide the IBAN (or sort code + account number for UK accounts), the account
holder name (must match your registered business name), and a human-readable
label for internal use.
### Verify ownership [#verify-ownership]
Depending on the rail, ARYZE may run a micro-deposit or account-name check
before activating the destination. Follow the on-screen instructions.
## Sandbox vs live destinations [#sandbox-vs-live-destinations]
Destinations, like API keys, are scoped to an environment:
* **Sandbox destinations** don't move real money. They exist so you can
exercise the full API surface during development.
* **Live destinations** require an approved KYB submission on the
organisation.
Creating a live API key requires both an approved KYB submission **and** an
active live destination in the matching currency. If either is missing, the
"Create API key" button is disabled and the dashboard tells you what's left.
# Getting started (/docs/saas-dashboard/getting-started)
Complete these steps once per organisation. After that, everything else —
destinations, more API keys, inviting teammates — is incremental.
### Create an account [#create-an-account]
Sign up with your work email. You'll be asked to verify your email address
before you can sign in. A personal organisation is created for you
automatically.
### Enable two-factor authentication [#enable-two-factor-authentication]
2FA is **required** before you can create live API keys. Add an authenticator
app under **Account → Security**.
### Create a business organisation [#create-a-business-organisation]
Under **Account → Organisations**, create an organisation for your company.
All merchants, destinations, and API keys belong to an organisation, not to
you personally — this is what lets you invite teammates later.
### Submit KYB [#submit-kyb]
Open **Account → Verification** and start the KYB flow. You'll be walked
through document upload and a beneficial-owner check. See
[Verification](/docs/saas-dashboard/verification) for what to have ready.
You can continue integrating in sandbox while KYB is reviewed. Live access
unlocks once the assessment is approved.
### Register your merchant [#register-your-merchant]
Go to **Account → Merchant** and fill in your company's details: legal name,
registration number, country of incorporation, and contact information.
This is the record your KYB assessment will be run against, so make sure the
details match what's on your company registration documents.
### Add a bank destination [#add-a-bank-destination]
Under **Open Finance → Banks**, connect the bank account where ARYZE should
settle your payments. Each destination is tied to a currency and a payment
rail (e.g. SEPA, Faster Payments).
See [Bank destinations](/docs/saas-dashboard/bank-destinations).
### Create your first API key [#create-your-first-api-key]
Under **Open Finance → API Keys**, click **Create API key**. Pick a
destination, set a webhook URL, and copy the client secret — it's shown
**once**. See [API keys](/docs/saas-dashboard/api-keys) for details.
### Send a sandbox payment [#send-a-sandbox-payment]
Head to **Open Finance → Playground** to send a test payment using your new key
without setting up a real checkout. Or follow the
[Quickstart](/docs/getting-started) to call the API directly.
### Start accepting payments [#start-accepting-payments]
Wire your backend to [`POST /v1/auth/token`](/docs/api-reference/payment-api/create-access-token)
and [`POST /v1/payments/initiate`](/docs/api-reference/payment-api/initiate-payment),
and point your webhook URL at a handler that listens for
[`payment.status_updated`](/docs/api-reference/payment-api/webhook-events/payment-status-updated).
Follow the [Accept a payment](/docs/guides/accept-a-payment) guide for the
full end-to-end flow.
# Dashboard (/docs/saas-dashboard)
The ARYZE Open Finance Payments Dashboard is where you set up everything the Payment API needs
to work: your organisation, KYB verification, bank destinations, API keys, and
webhook URLs. It's also where you monitor transactions and invoices day-to-day.
## Environments [#environments]
The dashboard exposes two environments side by side:
* **Sandbox** — for development. No real money moves. Minimal verification
required. Use this while you integrate.
* **Live** — real payments. Requires 2FA on your account, an approved KYB
submission, and at least one configured bank destination.
API keys are scoped to a single environment. Switch environments using the
selector in the dashboard header; each environment has its own API keys,
destinations, and transactions.
# Sandbox (/docs/saas-dashboard/sandbox)
The sandbox is a full working copy of the Payment API and dashboard. Nothing
moves real money — it's there so you can build and rehearse before going
live.
## What's the same as live [#whats-the-same-as-live]
* Every Payment API endpoint, request shape, and response field.
* Webhook delivery to your webhook URL, with the same retry behaviour.
* Dashboard pages (transactions, reports, destinations, API keys).
## What's different [#whats-different]
* API keys, destinations, and transactions are separate from live — a
sandbox key can't create live payments and vice versa.
* No KYB approval is required to create sandbox keys.
* No funds ever move. "Completed" sandbox payments are marked completed by a
test harness.
## Send a sandbox payment [#send-a-sandbox-payment]
### Option 1 — Dashboard Playground [#option-1--dashboard-playground]
**Open Finance → Playground** has a built-in form that calls
`/v1/payments/initiate` on your behalf and lets you step through each status
transition. Useful for:
* Testing your webhook handler end-to-end.
* Triggering failure scenarios (`FAILED` status, timeouts).
* Demoing the flow to non-technical teammates.
Prerequisites:
* A sandbox bank destination configured.
* An active sandbox API key.
### Option 2 — Call the API from your own code [#option-2--call-the-api-from-your-own-code]
Use your sandbox `client_secret` with the same base URL as live
() — the `environment` returned in the token response tells you
which side you're on. Follow the [Quickstart](/docs/getting-started) exactly.
# Transactions (/docs/saas-dashboard/transactions)
Every payment created through the API — by your backend, by the sandbox
simulator, or by another tool using the same API key — appears in **Open
Finance → Transactions**.
## The list view [#the-list-view]
The table shows the most recent payments with:
* Transaction ID (the same one the API returns).
* Merchant reference.
* Amount and currency.
* Current status — see [Status values](#status-values) below.
* Destination bank.
* Initiation and last-updated timestamps.
Filter by status, date range, destination, or free-text search against
reference and transaction ID.
## Status values [#status-values]
The status filter at the top of the list also offers `CANCELLED`, but no
transaction ever sits in that state — Mastercard `CANCELLED` is recorded on
the transaction's Mastercard status while the ARYZE status is set to
`FAILED`. Filter by `FAILED` and check the Mastercard status column to find
cancellations.
## The detail view [#the-detail-view]
Click any row to see:
* The Mastercard payment ID and raw Mastercard status — useful when opening a
support ticket.
* The destination bank, the API credential that initiated the payment, and the
redirect / flow URLs issued to the customer.
* The metadata you passed at initiation, decoded and rendered.
For an authoritative state check, call
[`GET /v1/payments/{transactionId}`](/docs/api-reference/payment-api/get-payment).
The dashboard reflects the same record.
## Investigating a failed payment [#investigating-a-failed-payment]
1. Open the transaction.
2. Look at the **Mastercard status** — that's the raw upstream reason (for
example `CANCELLED` or `REJECTED`).
3. Compare against your own webhook log. ARYZE retries failed deliveries up
to four times (1s / 5s / 15s); if your endpoint never returned 2xx, the
transaction status in the dashboard is still authoritative.
4. Still unclear? Contact support with the **ARYZE transaction ID** and
**Mastercard payment ID**.
## Reconciling [#reconciling]
For bulk reconciliation, compare transaction IDs and final status against
your own order table. Everything in the dashboard maps 1:1 to what
[`GET /v1/payments/{transactionId}`](/docs/api-reference/payment-api/get-payment) returns, so you can
automate reconciliation with the API and keep the dashboard for ad-hoc
investigation.
# Verification (KYB) (/docs/saas-dashboard/verification)
ARYZE is a regulated financial service. Before you can accept real payments,
your business needs to pass **Know Your Business (KYB)** verification. KYB
happens in the dashboard under **Account → Verification**.
You can keep integrating in **sandbox** while KYB is under review. Only the
live environment is gated behind approval.
## What you'll need [#what-youll-need]
* Company registration number and country of incorporation
* A PDF of your certificate of incorporation (or equivalent)
* Details of each beneficial owner holding ≥25%: full name, date of birth,
residential address, and a government-issued ID
* Proof of operating address (utility bill or bank statement dated within the
last 3 months)
* A high-level description of your business model, expected monthly payment
volume, and target geographies
## How to submit [#how-to-submit]
### Open the verification centre [#open-the-verification-centre]
Go to **Account → Verification**. You'll see the status of your KYB
submission and any outstanding items.
### Start the KYB assessment [#start-the-kyb-assessment]
Click **Start KYB**. You're taken through the assessment step by step —
business details first, then beneficial owners, then document upload.
You can pause and resume. Your progress is saved per organisation.
### Submit for review [#submit-for-review]
Once every section is complete, click **Submit**. You'll get an email when the
review finishes. Typical turnaround is a few business days.
### Respond to follow-ups [#respond-to-follow-ups]
If reviewers need more information — clearer documents, a missing owner —
you'll get a notification in the dashboard. Re-open the verification centre
to address each item and resubmit.
## Status values [#status-values]
The badge in the dashboard reflects the underlying KYB record. The label you see
maps to a single internal status code (shown in `code`).
# Create access token (/docs/api-reference/payment-api/create-access-token)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a payment (/docs/api-reference/payment-api/get-payment)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Payment API (/docs/api-reference/payment-api)
The Payment API is served from a single base URL and versioned under `/v1`:
All endpoints except [`POST /v1/auth/token`](./create-access-token) require a
Bearer JWT in the `Authorization` header. Exchange your API key's client
secret for a token at the auth endpoint, cache it for up to 60 minutes, and
send it as `Authorization: Bearer ` on every other call.
## Rate limits [#rate-limits]
| Endpoint | Policy | Limit |
| ---------------------------------------------------------------- | --------- | --------------------- |
| `POST /v1/auth/token` | `token` | 10 requests / minute |
| `POST /v1/payments/initiate`, `GET /v1/payments/{transactionId}` | `payment` | 100 requests / minute |
Exceeding a limit returns `429 Too Many Requests`.
## Conventions [#conventions]
* **Content type:** every request and response body is `application/json`.
* **Timestamps:** ISO 8601, UTC (`2026-04-21T14:30:00Z`).
* **Amounts:** decimal numbers in the payment currency (`100.50`, not `10050` minor units).
* **Currency:** ISO 4217 code (`EUR`, `GBP`, …).
* **Environment:** the same base URL serves sandbox and live — the environment is determined by the API key you authenticate with and is echoed back in the token response.
# Initiate a payment (/docs/api-reference/payment-api/initiate-payment)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Webhook Events (/docs/api-reference/payment-api/webhook-events)
Whenever a payment's state changes, ARYZE POSTs a JSON body to the webhook URL
on the API key you authenticated with. Each page below documents one event
type — its payload, retry semantics, and an example handler.
## Delivery semantics [#delivery-semantics]
* **Method:** `POST` with `Content-Type: application/json`.
* **Success:** any HTTP 2xx within 10 seconds.
* **Retries:** 1 initial delivery + 3 retries at 1 s, 5 s, 15 s.
* **Signature verification:** payloads are not signed today. Cross-check every event by fetching the transaction from [`GET /v1/payments/{transactionId}`](/docs/api-reference/payment-api/get-payment) before acting on it.
See [Consume webhooks](/docs/guides/consume-webhooks) for an end-to-end
handler with idempotency and error handling.
# Payment status updated (/docs/api-reference/payment-api/webhook-events/payment-status-updated)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}