Payment Docs
Payouts

One-click payouts (widget)

Clients link a bank account once in an embedded widget and receive the next payouts in one click

Before you start

This payout destination is not enabled for all merchants. Check availability with your manager, and make sure your merchant balance is sufficient before creating a payout.

Overview

The payout widget is a page you embed on your site in an iframe (or open in a popup). It pays out to a ruble account of an individual in a Russian bank, the same BANK_ACCOUNT payout as in Russia (bank account), but your client enters the requisites in the widget instead of your site collecting them:

  • First payout. The client fills in their full name, the phone number linked to the account, the account number, the bank BIK and, optionally, the date of birth. We save the requisites for this client and create the payout.
  • Next payouts. The widget shows the saved account (bank logo, the last four digits, the name) and the client confirms the payout with one button. The client can also replace the requisites before confirming.

Requisites are stored per client: your merchant plus the userId you pass. The widget speaks Russian and uses a light theme. It never shows why a payout failed: you get the reason from your balance, the service limits or your manager, not the client.

CurrencyDestinationPayment type
RUBAccount of an individual in a Russian bankBANK_ACCOUNT

Flow

Build the payout parameters

On your backend, when the client asks for a payout, collect the parameters into a JSON object:

{
  "merchantId": "0198c3f1-1111-7222-8333-444455556666",
  "userId": "user_12345",
  "amount": "1500.00",
  "currency": "RUB",
  "externalId": "payout-123",
  "serviceId": 123,
  "expiresAt": 1790000000
}

The fields are described in Payload fields.

Sign it

Encode the JSON in base64url (the payload) and sign exactly that string with your API token: signature is the HMAC-SHA512 of payload, in hex — the same algorithm and key as webhook signatures. See Signing.

Open the widget

Put both values into the widget URL and show it in an iframe or a popup:

https://pay.platinout.com/payout-widget?payload={payload}&signature={signature}

See Embedding.

Follow the widget events

The widget reports every step to your page with postMessage: when the requisites are saved, when the payout is created or fails, when the client presses «Закрыть». Use them to resize and close the iframe and to update your interface. See Events.

Track the payout by webhooks

The widget creates a regular payout with the externalId from the link. Its statuses arrive with the usual payout webhooks (X-Type: PAYOUT_UPDATE), and you can poll it by GET /v1/payouts/external/{externalId}. The webhook, not a widget event, is the source of truth for the payout result.

Payload fields

All fields except serviceId are required. Unknown fields are rejected, and string fields must not contain control characters (line breaks and the like).

FieldTypeDescription
merchantIdstring (UUID)Your merchant ID: merchantId from GET /v1/merchants/balance, also present in every webhook
userIdstringThe client ID in your system, up to 255 characters. Requisites are saved per userId: use your stable internal ID, never a value the client can choose
amountstring or numberPayout amount in rubles, from 1 to 99 999 999.99, at most two decimals. A string ("1500.00") is safest: floating-point numbers like 0.30000000000000004 are rejected
currencystringRUB
externalIdstringYour unique payout ID, up to 255 characters, same rules as in POST /v1/payouts. One link creates at most one payout: generate a new externalId for every payout
serviceIdintegerBank account payout service from GET /v1/payouts/services. When omitted, your default payout service is used — make sure it handles BANK_ACCOUNT
expiresAtintegerWhen the link expires, Unix time in seconds. At most 24 hours ahead; we recommend 15–30 minutes, enough for the client to find their requisites

Signing

payload is the base64url encoding (RFC 4648 §5, - and _ instead of + and /, padding optional) of the UTF-8 JSON. signature is hex(HMAC-SHA512(apiToken, payload)), where payload is the encoded string, not the JSON. Both values are URL-safe and go into the URL as is.

Sign on your backend only. The API token is a secret: it must never reach the browser, and so the signature cannot be computed in client-side code.

import crypto from "crypto";

const params = {
  merchantId: process.env.MERCHANT_ID,
  userId: "user_12345",
  amount: "1500.00",
  currency: "RUB",
  externalId: "payout-123",
  serviceId: 123,
  expiresAt: Math.floor(Date.now() / 1000) + 30 * 60, // 30 minutes
};

const payload = Buffer.from(JSON.stringify(params)).toString("base64url");
const signature = crypto
  .createHmac("sha512", process.env.API_TOKEN)
  .update(payload)
  .digest("hex");

const widgetUrl = `https://pay.platinout.com/payout-widget?payload=${payload}&signature=${signature}`;
import base64, hashlib, hmac, json, os, time

params = {
    "merchantId": os.environ["MERCHANT_ID"],
    "userId": "user_12345",
    "amount": "1500.00",
    "currency": "RUB",
    "externalId": "payout-123",
    "serviceId": 123,
    "expiresAt": int(time.time()) + 30 * 60,  # 30 minutes
}

raw = json.dumps(params, separators=(",", ":")).encode()
payload = base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
signature = hmac.new(os.environ["API_TOKEN"].encode(), payload.encode(), hashlib.sha512).hexdigest()

widget_url = f"https://pay.platinout.com/payout-widget?payload={payload}&signature={signature}"
<?php
$params = [
    'merchantId' => getenv('MERCHANT_ID'),
    'userId' => 'user_12345',
    'amount' => '1500.00',
    'currency' => 'RUB',
    'externalId' => 'payout-123',
    'serviceId' => 123,
    'expiresAt' => time() + 30 * 60, // 30 minutes
];

$json = json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$payload = rtrim(strtr(base64_encode($json), '+/', '-_'), '=');
$signature = hash_hmac('sha512', $payload, getenv('API_TOKEN'));

$widgetUrl = "https://pay.platinout.com/payout-widget?payload={$payload}&signature={$signature}";

Check your implementation

With the API token YOUR_API_TOKEN and the parameters from the first step, the values must be:

payload   = eyJtZXJjaGFudElkIjoiMDE5OGMzZjEtMTExMS03MjIyLTgzMzMtNDQ0NDU1NTU2NjY2IiwidXNlcklkIjoidXNlcl8xMjM0NSIsImFtb3VudCI6IjE1MDAuMDAiLCJjdXJyZW5jeSI6IlJVQiIsImV4dGVybmFsSWQiOiJwYXlvdXQtMTIzIiwic2VydmljZUlkIjoxMjMsImV4cGlyZXNBdCI6MTc5MDAwMDAwMH0
signature = 7c39e6f057794d8efdcf2b8859daa91bfdab06caf59e940ecb4d20e4905af2d1c8ef1cdbe6c0d9ab16f4c44deca24bcff3663e6123bc7eafd20ea20176045109

The JSON may be serialized differently by your language (key order, spaces): that is fine, as long as you sign the very payload string you put into the URL.

Embedding

<iframe
  id="payout-widget"
  src="https://pay.platinout.com/payout-widget?payload=...&signature=..."
  title="Выплата"
  style="width: 100%; max-width: 440px; height: 560px; border: 0"
></iframe>
  • Size. The widget adapts to widths from 320 px; 360–480 px suits it best. Its height changes from screen to screen: set the iframe height from the resize event instead of a fixed value.
  • Popup. You can open the same URL with window.open(widgetUrl, "payout", "width=480,height=720"). The events then go to window.opener, and «Закрыть» closes the popup itself.
  • Repeated opening. A link whose payout is already created shows its current state (accepted, completed or failed) instead of the form, so a reload never creates a second payout.
  • Replacing requisites. The client can replace the saved requisites in the widget until the payout of the link is created. After that, the link no longer changes them.

Events

The widget posts messages to the page that embeds it (the iframe parent) or opened it (the popup opener):

{ "source": "payout-widget", "type": "payout_created", "data": { "externalId": "payout-123", "payoutId": "0198c3f1-2222-7333-8444-555566667777", "status": "CREATED" } }
typedataWhen
ready{ hasRequisites: boolean, payout: { id, status } | null }The link is valid and the first screen is shown. hasRequisites tells whether the client has saved requisites; payout is the payout of this link if it already exists
resize{ height: number }The content height in CSS pixels changed. Set it as the iframe height
requisites_saved{ externalId: string, changed: boolean }The client linked requisites for the first time (changed: false) or replaced saved ones (changed: true)
payout_created{ externalId: string, payoutId: string, status: string }The payout is created, usually with the CREATED status. The final status comes with webhooks
payout_failed{ externalId: string, payoutId: string | null }The payout was not created (payoutId: null, the client can retry in the widget) or was created and failed at once. No reason is given
error{ code: string, detail?: string }The link cannot be used or the service is unavailable. Codes are listed below
close{}The client pressed «Закрыть» on a final screen. Close the iframe or the popup

Check the message origin before trusting it:

const WIDGET_ORIGIN = "https://pay.platinout.com";
const frame = document.getElementById("payout-widget");

window.addEventListener("message", (event) => {
  if (event.origin !== WIDGET_ORIGIN) return;
  const message = event.data;
  if (!message || message.source !== "payout-widget") return;

  switch (message.type) {
    case "resize":
      frame.style.height = `${message.data.height}px`;
      break;
    case "payout_created":
      // Show "payout accepted"; wait for the webhook for the final status.
      break;
    case "payout_failed":
    case "error":
      // Let the client try again later, or log message.data.code.
      break;
    case "close":
      frame.remove();
      break;
  }
});

Events are for your interface only. Credit or release your client's balance by the payout webhook, which is signed, never by a postMessage event.

error codes

codeMeaningWhat to do
PAYLOAD_INVALIDThe payload cannot be decoded, a field is missing or invalid. detail names the fieldFix the payload by Payload fields
SIGNATURE_INVALIDThe signature does not match, or merchantId is not yoursSign the exact payload string with the current API token
LINK_EXPIREDexpiresAt has passedCreate a new link
UNAVAILABLEThe service is temporarily unavailableLet the client retry later

Requisites check

The widget checks the requisites while the client types and again when they are saved, by the same rules as bank account payouts: the full name in Cyrillic, the phone in the +7XXXXXXXXXX format, a known BIK, and a 20-digit ruble account of an individual whose control key matches the BIK. The optional date of birth must give an age from 14 to 120 years.

Recommendations

  • Generate the link at the moment the client asks for a payout, with a short expiresAt, and a new externalId every time.
  • Do not log or share links: until the payout is created, a link lets its holder replace the client's requisites.
  • Reserve the amount on your side before opening the widget, and settle it by the payout webhook.

Next steps

On this page