Flow: bank card binding
Card-on-file / custom form: card data is entered on the merchant’s website or in the app and passed to the API inside <BankCardCustomer>. The card is bound via a contract with no PIN confirmation; subsequent charges go through without re-entering the card details. The scenario exists only in the XML protocol — JSON v3 has no block for raw card data.
Two hard conditions. The merchant handling raw card data must be PCI DSS certified — without it, use the regular card flow with a redirect to the payment form. The presence of the BankCard block in requests is governed by the integration type and the individual requirements of the acquirer — it is enabled per partner.
Sandbox
Only test card numbers starting with 1111 (e.g. 1111 2222 3333 4444) — never send real card data. The reserved endings 0001–0005 emulate a charge refusal. The sandbox does not store card data (only the last 4 digits of the PAN remain), the CVC is never stored in any form, and card details are masked in the request log. Sandbox contracts are cleaned up by the regular retention, not by the two-year term from the spec.
- Bind the card:
CreateCustomerContractwith<BankCardCustomer>containing the full<BankCard>block (PAN, expiration, CVC, cardholder name) plusMerchantContractID. Headers for this scenario:Service-Method: CreateCustomerContract,Callisto-Protocol-Version: 2,SignatureVersion: 4. The response returnsContractID,AuthorizationType: SKIPand the contractExpirationTimeUtc: there is no PIN confirmation as in the mobile phone flow, theAuthorizeCustomerContractstep is not needed. - Charge under the contract: a regular
CreateInvoicewith the same<BankCardCustomer>and<CustomerContractInfo>(MerchantContractID);CreateIfNotExists=truemay create the contract together with the first invoice. The response contains theCustomerContractInfoblock andAuthorizationInfowith anAuthorizationUrl— the 3-D Secure confirmation page (any 4-digit code works in the sandbox); the card form itself stays on the merchant side. With 3-D Secure off the response looks different — see the note below. - Re-binding the same active
MerchantContractIDreturnsResultCode230(Business_CustomerContractAlreadyExists— see ResultCode) with the existingContractIDinResultDescription.
Step 1 — CreateCustomerContract with card details (XML)
<Request>
<Header><Authentication>
<Login>demo_login</Login>
<NowUtc>20260625120000</NowUtc>
<Signature>abcdef1234...</Signature>
</Authentication></Header>
<CreateCustomerContractRequest>
<ProjectID>1001</ProjectID>
<ClientIP>10.20.30.40</ClientIP>
<BankCardCustomer>
<Source>my_source</Source>
<Country>ru</Country>
<MerchantCustomerId>user_123</MerchantCustomerId>
<Email>test@test.com</Email>
<BankCard>
<PAN>1111222233334444</PAN>
<ExpirationMonth>08</ExpirationMonth>
<ExpirationYear>28</ExpirationYear>
<SecurityCode>123</SecurityCode>
<Cardholder>IVANOV IVAN</Cardholder>
</BankCard>
</BankCardCustomer>
<CustomerContractInfo>
<MerchantContractID>CARD-CONTRACT-001</MerchantContractID>
</CustomerContractInfo>
</CreateCustomerContractRequest>
</Request><Response>
<CreateCustomerContractResponse>
<BankCardCustomer>
<Source>my_source</Source>
<Country>ru</Country>
<MerchantCustomerId>user_123</MerchantCustomerId>
<Email>test@test.com</Email>
</BankCardCustomer>
<CustomerContractInfo>
<ContractID>1c6495bb-0002-005f-f54e-d286731d55b9</ContractID>
<AuthorizationType>SKIP</AuthorizationType>
<ExpirationTimeUtc>27.04.2028 21:00:06</ExpirationTimeUtc>
<MerchantContractID>CARD-CONTRACT-001</MerchantContractID>
</CustomerContractInfo>
</CreateCustomerContractResponse>
<ResultCode>0</ResultCode>
<ResultDescription>OK</ResultDescription>
</Response>Step 2 — charge under the contract (XML)
<Request>
<Header><Authentication>
<Login>demo_login</Login>
<NowUtc>20260625120000</NowUtc>
<Signature>abcdef1234...</Signature>
</Authentication></Header>
<CreateInvoiceRequest>
<ProjectID>1001</ProjectID>
<ClientIP>10.20.30.40</ClientIP>
<BankCardCustomer>
<Source>my_source</Source>
<Country>ru</Country>
<MerchantCustomerId>user_123</MerchantCustomerId>
<Email>test@test.com</Email>
</BankCardCustomer>
<InvoiceInfo>
<MerchantOrderID>ORDER-001</MerchantOrderID>
<CustomerPrice amount="99.00" currency="RUB" includingVAT="true"/>
</InvoiceInfo>
<CustomerContractInfo>
<CreateIfNotExists>false</CreateIfNotExists>
<MerchantContractID>CARD-CONTRACT-001</MerchantContractID>
</CustomerContractInfo>
</CreateInvoiceRequest>
</Request>Sandbox
<Response>
<CreateInvoiceResponse>
<BankCardCustomer>
<Source>my_source</Source>
<MerchantCustomerId>user_123</MerchantCustomerId>
<Country>ru</Country>
<Email>test@test.com</Email>
</BankCardCustomer>
<InvoiceInfo>
<CustomerPrice amount="99.00" currency="RUB" includingVAT="true" />
<CreatedTimeUtc>31.08.2026 20:00:10</CreatedTimeUtc>
<ExpirationTimeUtc>31.08.2026 22:00:10</ExpirationTimeUtc>
<InvoiceStatus>WaitPaymentConfirmation</InvoiceStatus>
<PaymentStatus>NOT_PAID</PaymentStatus>
<AuthorizationType>AUTH_PAGE</AuthorizationType>
<IsAuthorized>false</IsAuthorized>
<InvoiceID>3c8d95c6-0006-0062-6727-4ea756dba332</InvoiceID>
<MerchantOrderID>ORDER-001</MerchantOrderID>
</InvoiceInfo>
<BillingInfo>
<BillingChannel>BankRtp</BillingChannel>
</BillingInfo>
<CustomerContractInfo>
<ContractID>1c6495bb-0002-005f-f54e-d286731d55b9</ContractID>
<AuthorizationType>SKIP</AuthorizationType>
<ExpirationTimeUtc>27.04.2028 21:00:06</ExpirationTimeUtc>
<MerchantContractID>CARD-CONTRACT-001</MerchantContractID>
</CustomerContractInfo>
<AuthorizationInfo>
<AuthorizationUrl>https://pay-sandbox.digitalspb.com/checkout/a038a5ce-c580-4ff1-b480-e364b21a7474/pin?last4=4444</AuthorizationUrl>
</AuthorizationInfo>
</CreateInvoiceResponse>
<ResultCode>0</ResultCode>
<ResultDescription>OK</ResultDescription>
</Response>The response depends on the card_3ds_enabled setting. This is the only flow where 3-D Secure changes not just the number of screens the payer sees but the response payload as well. With the setting off the charge is settled synchronously inside CreateInvoice: there is no AuthorizationInfo block (and so no payment link), the invoice AuthorizationType is SKIP, and InvoiceStatus / PaymentStatus already carry the outcome (Processed / PAID, or Cancelled / NOT_PAID for a reserved card ending). The payment notification is sent as usual. Both responses side by side and how to test them — Flow: 3-D Secure.
Variant — card details directly in CreateInvoice (XML)
Depending on the integration, the <BankCard> block is passed not only in CreateCustomerContract but also in CreateInvoice — inside the same <BankCardCustomer>, with the same fields as in step 1. The same conditions apply: PCI DSS certification and individual enablement by the acquirer (see the callout at the top of the page).
<Request>
<Header><Authentication>
<Login>demo_login</Login>
<NowUtc>20260625120000</NowUtc>
<Signature>abcdef1234...</Signature>
</Authentication></Header>
<CreateInvoiceRequest>
<ProjectID>1001</ProjectID>
<ClientIP>10.20.30.40</ClientIP>
<BankCardCustomer>
<Source>my_source</Source>
<Country>ru</Country>
<MerchantCustomerId>user_123</MerchantCustomerId>
<Email>test@test.com</Email>
<BankCard>
<PAN>1111222233334444</PAN>
<ExpirationMonth>08</ExpirationMonth>
<ExpirationYear>28</ExpirationYear>
<SecurityCode>123</SecurityCode>
<Cardholder>IVANOV IVAN</Cardholder>
</BankCard>
</BankCardCustomer>
<InvoiceInfo>
<MerchantOrderID>ORDER-002</MerchantOrderID>
<CustomerPrice amount="99.00" currency="RUB" includingVAT="true"/>
</InvoiceInfo>
</CreateInvoiceRequest>
</Request>Recurring payments by phone number (a contract with PIN confirmation) are a separate flow: Flow: recurring. The full method description — Create contract and the createContract reference.