Flow: recurring payments
A contract is a one-time consent from the subscriber, after which charges go through without a PIN for every payment. The subscriber confirms once, and from then on you create invoices referencing the contract.
- Create the contract:
POST /contractswithcustomerandmerchant_contract_id. The response is201withcontract_id,authorization_typeandexpires_at; it has nostatusfield. - Confirm it:
POST /contracts/{contract_id}/authorizewith"pin_code": "1234". The response carriescontract_idandstatus: active, followed by acontract.callback.authorized - Charge against the contract: an ordinary
POST /invoices, but with acontract: {"merchant_contract_id": "…"}block. No PIN is requested at this step. - Check the outcome: with
SuccessSequencePaymentin the sandbox knob the invoice reachesProcessed/PAID, withFailedSequencePaymentthe transaction fails (see “Sandbox knobs”). In thepayment.callback the outcome is read fromupdated invoice.,status invoice.andpayment_status transaction.; in the legacy envelope (status payment./completed payment.) the value you set also arrives as thefailed billing_resultfield. - Close the contract when the subscription ends:
POST /contracts/{contract_id}/cancelwith an empty body. The response isstatus: cancelled, followed by acontract.callback.closed
Where merchant_contract_id goes depends on the method. In POST /contracts it sits at the top level of the body, in POST /invoices it sits inside the contract block. The mock does not validate the body: put the field in the wrong place and you get 201 with an empty value in the echoed fields of the response.
The sandbox stores mobile contracts, as production does: a repeated merchant_contract_id gets 409, actions on an unknown or cancelled contract get 404 CONTRACT_NOT_FOUND (see “JSON v3 errors: HTTP codes”). The authorization block comes back even for a charge against a contract — that is a quirk of the mock, not a request to confirm the payment. Details in “Sandbox ↔ Production differences”.
// POST /api/v3/projects/1001/contracts
{
"customer": { "type": "mobile_phone", "msisdn": "79600000000" },
"merchant_contract_id": "CONTRACT-599160" // top level!
}
// 201 Created
{
"success": true,
"data": {
"contract_id": "85ff8da2-2b75-44f9-b012-49927da62fde",
"authorization_type": "SMS_PIN",
"expires_at": "2026-09-21T12:00:00Z",
"merchant_contract_id": "CONTRACT-599160",
"customer": { "type": "mobile_phone", "msisdn": "79600000000" }
}
}// POST /contracts/{contract_id}/authorize
{ "pin_code": "1234" }
// 200 OK → { "contract_id": "85ff8da2-…", "status": "active" }
// callback (excerpt):
{
"type": "contract.authorized",
"data": {
"customer": { "type": "mobile_phone", "msisdn": "79600000000" },
"contract": {
"contract_id": "85ff8da2-…",
"merchant_contract_id": "CONTRACT-599160"
}
}
}// POST /api/v3/projects/1001/invoices
{
"customer": { "type": "mobile_phone", "msisdn": "79600000000" },
"order": {
"merchant_order_id": "SEQ-a9e56f",
"price": { "amount": "199.00", "currency": "RUB", "vat_included": true }
},
"contract": { "merchant_contract_id": "CONTRACT-599160" }, // inside contract!
"test_info": { "expected_billing_result": "SuccessSequencePayment" }
}{
"type": "payment.updated",
"data": {
"invoice": {
"invoice_id": "efa72de6-…",
"merchant_order_id": "SEQ-a9e56f",
"status": "Processed",
"payment_status": "PAID"
},
"transaction": { "status": "SUCCESS", "result_code": 0 }
// excerpt: customer and the other transaction fields are omitted
}
}