Paid checkout flow via Platform API and confirmation URL.
Payments
Flow
1. User selects a paid plan_price on the paywall
2. App calls createPayment(plan_price_id, return_url)
3. API creates provider payment + pending Platform payment row
4. App opens confirmation_url
5. Provider webhook → Platform marks succeeded → grants entitlements / subscription period
(if webhook missed: worker ~1 min, or getMyEntitlements / syncPayment)
6. App refreshes entitlements (and updates local cache)
If the webhook never arrived but YooKassa already shows succeeded, Platform closes pending on its own:
- worker every minute (
POST /v1/internal/payments/reconcile) getMyEntitlements()after return- SDK
syncPayment(payment_id)/POST /v1/payments/{id}/sync
Same path as the webhook: verify at YooKassa → finalize_yookassa_payment. Admin Payments → Sync with YooKassa remains for debugging.
Client rules
- Require auth JWT + public key
- Never send provider secrets from the client
- Do not call createPayment for free prices
- After return —
getMyEntitlements()(reconciles pending). OptionalsyncPayment(payment_id)
Webhook (production)
YooKassa cabinet URL: YOUR_API_URL/v1/webhooks/yookassa (API host, not marketing).
Finalize reads the token from platform_config.yookassa_webhook_token (≥24 chars in production). Env YOOKASSA_WEBHOOK_TOKEN is a fallback if the DB token is still weak. The YooKassa cabinet webhook URL must get a non-2xx until our payment row exists, so YooKassa retries.
Reconcile (admin)
POST /v1/payments/{paymentId}/sync
POST /v1/payments/sync-pending
Canceled in YooKassa → canceled. If the plan was already granted via admin/license, sync only closes the pending row (no second grant).
Response (paid)
{
"data": {
"payment_id": "uuid",
"provider_payment_id": "…",
"status": "pending",
"confirmation_url": "https://…",
"amount": 1490,
"currency": "RUB",
"requires_purchase": true
}
}
Free attempt
{
"error": "free_plan_offline",
"is_free": true,
"requires_purchase": false,
"works_offline": true
}
Display discount
discount_percent is cosmetic. Charged amount is always amount. Strike-through uses compare_at_amount.