Hosted Checkout (Payform)
A white-label payment page at checkout.syncra.money — how to accept payments without building your own frontend
Hosted Checkout (Payform)
Syncra provides a hosted payment page — a single-page application on
the checkout.syncra.money domain, branded for your merchant. You do not
need to develop a payment form, lay out the trader's payment details, or
count down timers: you simply create a PayIn, get a link, and send the
client to it.
It is the fastest way to integrate — a full-featured payment page available "out of the box".
What it is
Payform is a white-label payment page that opens at:
https://checkout.syncra.money/pay/{hash}where {hash} is a 64-character hex identifier of the deal (an
unguessable capability token). At this link, Syncra shows the client:
- the P2P trader's payment details: a card, an SBP phone, or an IBAN for international bank transfers — depending on the deal's requisite type;
- the exact transfer amount and a countdown timer;
- step-by-step payment instructions for their banking app;
- the "I paid" button — pressing it starts the verification of the received funds;
- your branded logo, colors, and name in the header.
The page is anonymous: the client never sees internal platform
identifiers (trader_id, team_id, provider_id, fees). Payform
receives only a "projection" of the deal — enough to pay, without
revealing the P2P network architecture.
How to get the URL
payment_url is returned in the response to CreateMerchantPayIn (proto
field 9 in MerchantDealInfo):
POST https://api.syncra.money/api/v1/p2p/merchant/deals/payinResponse example (200 OK):
{
"deal": {
"deal_id": "7ac148fe-19a3-45bb-b992-019fac55b721",
"status": "ESCROW_LOCKED",
"amount": "150000",
"currency": "RUB",
"payment_url": "https://checkout.syncra.money/pay/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"created_at": "2026-08-03T12:00:00Z"
}
}The base domain (checkout.syncra.money) is set by the
checkout_base_url tenant-level setting. For most integrations, the
domain is already configured — you simply use the returned payment_url
as is.
You can pass payment_url between your backend and frontend without
restrictions. The link itself is the client's "authorization": it is
unguessable, and no separate API tokens are needed for the payment
page.
Redirecting the client
After creating a PayIn, send the client to payment_url. A few typical
options:
// Browser: once the backend has returned payment_url, redirect the client.
const res = await fetch('/api/deposit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 150000 }),
});
const { payment_url } = await res.json();
window.location = payment_url;app.post('/api/deposit', async (req, res) => {
const deal = await syncra.createPayIn({
amount: req.body.amount,
currency: 'RUB',
idempotency_key: crypto.randomUUID(),
client_id: req.user.id,
});
res.json({ payment_url: deal.payment_url });
});The client completes the payment on the payment page, after which Syncra automatically redirects them back to your site (see the Redirect URLs section) and sends you a webhook.
What the client sees
As the payment progresses, the page changes its state. Here is the sequence of screens:
1. Active payment (ESCROW_LOCKED)
The main screen with the payment details:
- The trader's card number / phone number (with a "Copy" button);
- The card holder's full name;
- The transfer amount in large font;
- A countdown timer (usually ~15 minutes, configured at the merchant level);
- A step-by-step instruction — how to make the transfer in the banking app;
- The "I paid" button — the client presses it after transferring.
IBAN transfers (BANK_TRANSFER)
For international markets (TRY, EUR, and others), an IBAN
(ISO 13616) is used instead of a card/phone — the recipient's
international account number. The client sees:
- The recipient IBAN (with a "Copy" button);
- The account holder's full name (
holder_name); - The bank/channel name (
payment_method_name) — for example, the recipient bank or the transfer scheme (Havale, SEPA); - The transfer amount;
- A formula-style instruction like:
"Transfer {amount} to IBAN {iban} for beneficiary {holder_name} via {bank_name}".
These fields come from CheckoutView (the payment page contract):
iban, holder_name, payment_method_name, amount, currency. The
IBAN is decrypted from the Vault with the same transit key as the PAN
and is zeroed in terminal states — exactly like card_number /
phone_number.
2. Waiting for confirmation (PAYMENT_NOTIFIED)
The client pressed "I paid". A spinner appears with the text "Waiting for confirmation from the bank". The page polls the status every ~5 seconds. No further action is required from the client.
3. Success (COMPLETED)
The payment is confirmed. A success screen; after ~3 seconds, the client
is automatically redirected to success_redirect_url.
4. Expiration / cancellation (EXPIRED / CANCELLED)
Time ran out or the payment was canceled. A "Time expired" screen; after
~3 seconds — a redirect to fail_redirect_url.
5. Dispute (SOFT_DISPUTED / APPEALED)
A dispute is open. A static "Awaiting the administration's decision" screen.
Branding
Payform is white-label. The entire page is re-skinned to your brand
through CSS variables, so restyling happens instantly, without a reload.
The configuration is stored in the merchant's brand_config field and
edited in the admin panel (/settings/branding).
brand_config keys
| Key | Type | Description |
|---|---|---|
primary_color | string (hex) | The main accent color. Example: #7c3aed. Affects buttons, stripes, and active elements. |
logo_url | string (https) | Header logo URL (https only). |
favicon_url | string (https) | Browser tab favicon URL. |
brand_name | string | Brand name, shown in the header next to the logo. |
theme | string | Color theme: "dark" (default) or "light". |
Configuration example
{
"primary_color": "#7c3aed",
"logo_url": "https://cdn.aurora-store.example/logo.svg",
"favicon_url": "https://cdn.aurora-store.example/favicon.ico",
"brand_name": "Aurora Store",
"theme": "dark"
}An empty brand_config object ({}) means using the standard Syncra
platform colors. Server-side validation: a hex color against a regex;
logo_url/favicon_url — https only.
Where to edit
In the Syncra admin panel, open Settings → Branding and fill in the fields. Changes take effect for all new deals instantly — you do not need to pass branding in the PayIn request or to rebuild the payform.
Redirect URLs
After a payment completes (or expires), Syncra automatically redirects
the client back to your site. These URLs are configured at the merchant
level in the admin panel and apply to all deals — they cannot be
overridden in the CreateMerchantPayIn request.
| Field (merchant level) | Where it leads |
|---|---|
success_redirect_url | The page after a successful payment (COMPLETED). |
fail_redirect_url | The page on failure/expiry (EXPIRED, CANCELLED). |
The CreateMerchantPayIn request has no success_url /
failed_url fields. These URLs are a merchant setting, not a protocol
field. This is intentional: redirect URLs pass moderation and must not
change from payment to payment. If you need a different success page
for different products, create several merchants.
Redirect behavior
- The redirect happens after ~3 seconds on the final screen, so the client has time to read the status.
- The redirect runs on the client's browser side (via
success_redirect_url/fail_redirect_url). - The server-side webhook (
deal.completed/deal.expired/deal.cancelled) is sent in parallel — do not rely on the redirect alone; always handle the webhook as the source of truth.
Statuses on the payment page
Payform shows a screen depending on the deal's status field
(CheckoutView.status):
| Deal status | Payform screen | Action |
|---|---|---|
ESCROW_LOCKED | Active payment: trader payment details (card / phone / IBAN), amount, timer, instructions, the "I paid" button. | The client makes the transfer. |
PAYMENT_NOTIFIED | Waiting for the trader's confirmation. Spinner, auto-polling every 5 sec. | The client waits. |
COMPLETED | Success. After ~3 sec — a redirect to success_redirect_url. | The deal.completed webhook sent. |
EXPIRED | Expiry. After ~3 sec — a redirect to fail_redirect_url. | The deal.expired webhook sent. |
CANCELLED | Cancellation (the "Cancel payment" button or cancellation by the merchant via API). After ~3 sec — a redirect to fail_redirect_url. | The deal.cancelled webhook sent. |
SOFT_DISPUTED / APPEALED | Dispute: "Awaiting the administration's decision". | The deal.appealed webhook sent. |
The full list of statuses and their numeric V1 codes is in the status reference. The full list of webhook events is in Callbacks.
Testing
For designers and product managers, payform has a demo page with all the states:
https://checkout.syncra.money/demoOn this page, you can switch between statuses (ESCROW_LOCKED,
PAYMENT_NOTIFIED, COMPLETED, EXPIRED, SOFT_DISPUTED) and see what
payform looks like with mock data — without a live backend.
The demo page is available only in dev-mode builds (the VITE dev
server). In the production build, the /demo route is not served. This
is intentional, so that end clients never see test screens.
Running payform locally
If you want to run payform locally against a sandbox backend:
# 1. Clone the core repo and install the dependencies.
pnpm install
# 2. Start payform in dev mode.
pnpm --filter @platform/payform dev
# → http://localhost:3002/pay/{hash} (payment page)
# → http://localhost:3002/demo (demo of all statuses)The dev server proxies /api/* requests to the gateway (localhost:8080
via VITE_PROXY_TARGET). Point it at your sandbox URL if the backend
runs on another host.
Related materials
- Quickstart — the full scenario, from creating a PayIn to receiving a webhook.
- Accepting payments (PayIn) — all request fields and deal statuses.
- Callbacks — webhook notifications that Syncra sends in parallel with the client redirect.
- Authentication — the HMAC-SHA256 signature of the PayIn creation request.