Payments & YooKassa
Balance top-up goes through YooKassa. The flow is:
- Create a payment → get a
confirmation_url. - Redirect the user to
confirmation_url. - YooKassa sends a signed webhook on success → balance is credited atomically.
Create a payment
curl -b cookies.txt -X POST https://api.runne.run/v1/payments \
-H 'Content-Type: application/json' \
-d '{"workspace_id":"0191…","amount_rubles":"500.00","description":"Top up my-app"}'Request body:
| Field | Type | Required | Notes |
|---|---|---|---|
workspace_id | string (UUID) | yes | must be owned by the session |
amount_rubles | string | yes | ^\d+(\.\d{1,2})?$, minimum 100.00 |
description | string | no | max 500 chars |
Response (HTTP 201):
{
"payment_id": "0191…",
"confirmation_url": "https://yoomoney.ru/checkout/payments/v2/contract?orderId=…",
"amount_rubles": "500.00",
"tokens_credited": "500.00",
"status": "pending"
}tokens_credited equals amount_rubles at the fixed beta rate (1 token = 1 ₽). The minimum amount is 100.00 ₽.
Redirect and pay
Send the user to confirmation_url. On completion YooKassa redirects back to the cabinet's success page and posts the webhook.
The webhook
POST /webhooks/yookassa is the only notification channel. It authenticates via the X-YooKassa-Signature header:
X-YooKassa-Signature = HMAC-SHA256(raw_body, YOOKASSA_WEBHOOK_SECRET)The comparison is constant-time. A missing or invalid signature returns 401 and the event is ignored.
Events
| Event | Effect |
|---|---|
payment.succeeded | Credits the balance (atomic), then sends a 54-ФЗ receipt |
payment.canceled | Marks the payment cancelled (only if still pending) |
receipt.succeeded | Records fiscal document/drive numbers |
receipt.canceled | Marks the receipt failed |
Idempotency
The webhook is safe against duplicate delivery through three layers:
- A status check — an already-
succeededpayment is skipped. - A conditional
UPDATE … WHERE status='pending'inside the transaction — only one concurrent handler wins. - A unique index on
transactions.payment_id— the credit row can only exist once.
Replaying a webhook therefore never double-credits a workspace. The amount is also re-verified against the payment row; a mismatch is audited and ignored.
Receipts (54-ФЗ)
Receipts are issued only for individual customers, as required by 54-ФЗ:
- A receipt is created after a successful payment and sent via YooKassa with a stable idempotence key (the payment id).
vat_codeis1(no VAT) in beta.- Receipt sending is best-effort: a send failure is audited and retried by a cron job, and does not fail the webhook.
Legal entities get an act/invoice instead of a receipt (not yet implemented in beta).
Cancellation
If the user abandons the checkout, YooKassa sends payment.canceled; the payment moves to cancelled and no balance is credited.
List payments
curl -b cookies.txt 'https://api.runne.run/v1/payments?workspace_id=0191…'{
"payments": [
{ "payment_id": "0191…", "amount_rubles": "500.00", "tokens_credited": "500.00", "status": "succeeded", "created_at": "…" }
]
}workspace_id is a required query parameter (HTTP 400 if missing). The response is scoped to the caller's workspace only.
Related
- Workspaces & Balance — how the credit lands.
- Errors — the
402 insufficient_balancepath. - Architecture — atomic financial operations.