Skip to content

Payments & YooKassa

Balance top-up goes through YooKassa. The flow is:

  1. Create a payment → get a confirmation_url.
  2. Redirect the user to confirmation_url.
  3. YooKassa sends a signed webhook on success → balance is credited atomically.

Create a payment

bash
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:

FieldTypeRequiredNotes
workspace_idstring (UUID)yesmust be owned by the session
amount_rublesstringyes^\d+(\.\d{1,2})?$, minimum 100.00
descriptionstringnomax 500 chars

Response (HTTP 201):

json
{
  "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

EventEffect
payment.succeededCredits the balance (atomic), then sends a 54-ФЗ receipt
payment.canceledMarks the payment cancelled (only if still pending)
receipt.succeededRecords fiscal document/drive numbers
receipt.canceledMarks the receipt failed

Idempotency

The webhook is safe against duplicate delivery through three layers:

  1. A status check — an already-succeeded payment is skipped.
  2. A conditional UPDATE … WHERE status='pending' inside the transaction — only one concurrent handler wins.
  3. 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_code is 1 (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

bash
curl -b cookies.txt 'https://api.runne.run/v1/payments?workspace_id=0191…'
json
{
  "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.