Bill a customer on a schedule
Charge a new subscriber, keep their card, and put them on a monthly contract the platform bills for you, instead of running the schedule from your own service.
Signed in, you can run this blueprint against your own sandbox merchant one call at a time, with the values from each call threaded into the next. Sign in to run it.
8 steps, 6 API callsPaymentsCustomersContractsRecurring Billing
Samples use {{API_KEY}} for your API key and {{BASE_URL}} for this instance's API address. Anything else in double braces is a value an earlier step gave you.
Charge the first period
1. Charge the first period at signup
API call
POST /api/transactions
Take the subscriber's first payment as an ordinary card sale, with the card number on the request. A schedule bills from its start date forward, so the period the customer is buying right now is yours to charge. Keep the merchant id off the response: the customer you create below belongs to it.
Values this step gives you
{{merchantId}}The merchant the sale belongs to, from the response body's merchantId property.
curl -X POST "{{BASE_URL}}/api/transactions" \
-H "api-key: {{API_KEY}}" \
-H "Content-Type: application/json" \
-d '{
"transactionType": "Sale",
"cardData": {
"cardNumber": "4111111111111111",
"nameOnCard": "Jane Doe",
"expirationMonth": 12,
"expirationYear": 2030,
"cvv": 123
},
"invoiceData": {
"amounts": { "base": 10.00, "total": 10.00 }
}
}'using var http = new HttpClient { BaseAddress = new Uri("{{BASE_URL}}") };
http.DefaultRequestHeaders.Add("api-key", "{{API_KEY}}");
var response = await http.PostAsJsonAsync("/api/transactions", new
{
transactionType = "Sale",
cardData = new
{
cardNumber = "4111111111111111",
nameOnCard = "Jane Doe",
expirationMonth = 12,
expirationYear = 2030,
cvv = 123
},
invoiceData = new
{
amounts = new { @base = 10.00m, total = 10.00m }
}
});
response.EnsureSuccessStatusCode();
var sale = await response.Content.ReadFromJsonAsync<JsonElement>();
var merchantId = sale.GetProperty("merchantId").GetString();What this step answers with
Abridged to the properties this step depends on. A real response carries more.
{
"id": "9f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
"merchantId": "3a7b1c9d-2e4f-4a6b-8c8d-9e0f1a2b3c4d",
"transactionType": "Sale",
"resultCode": "Ok",
"authorizedAmount": 10.00,
"responseData": {
"resultCode": "Ok",
"resultMessage": "Approved"
}
}Set the customer up
2. Choose a name for the subscriber
On your side
Pick the name the customer record is created under. It has to be unique within the merchant: this platform refuses a second customer with a name another one already holds, and answers "Customer name must be unique within the same merchant." That's why the page asks rather than showing one, and why a sandbox merchant other people work through has names on it already. Use a fresh one each time you come back here, and in your own integration use whatever your system calls the subscriber.
Values this step gives you
{{customerName}}The name the customer is created under, and the name on their contact detail. You choose it, so nothing reads it off a response.
3. Create the customer the schedule belongs to
API call
POST /api/customers
A contract bills a customer, so the customer record comes first. It's created under the name you chose above, which also becomes the primary contact name. Give the address list exactly one default entry with a street line, a city, a state and a postal code. The list may be left empty, but a partial address is refused. Keep the id, and keep the creation timestamp: the schedule below starts the day the subscriber signed up.
Values this step gives you
{{customerId}}The id of the created customer, from the response body's id property.{{scheduleStartDate}}The moment the customer record was created, from the response body's creationTime property. The contract below starts its schedule there.
curl -X POST "{{BASE_URL}}/api/customers" \
-H "api-key: {{API_KEY}}" \
-H "Content-Type: application/json" \
-d '{
"merchantId": "{{merchantId}}",
"name": "{{customerName}}",
"isActive": true,
"contactDetail": {
"primaryContactName": "{{customerName}}",
"emailAddress": "jane.doe@example.com",
"phone": "5555550123",
"addresses": [
{
"address1": "100 Main Street",
"city": "Minneapolis",
"state": "MN",
"zip": "55401",
"isDefault": true
}
]
}
}'var customerName = "{{customerName}}";
var created = await http.PostAsJsonAsync("/api/customers", new
{
merchantId,
name = customerName,
isActive = true,
contactDetail = new
{
primaryContactName = customerName,
emailAddress = "jane.doe@example.com",
phone = "5555550123",
addresses = new[]
{
new
{
address1 = "100 Main Street",
city = "Minneapolis",
state = "MN",
zip = "55401",
isDefault = true
}
}
}
});
created.EnsureSuccessStatusCode();
var customer = await created.Content.ReadFromJsonAsync<JsonElement>();
var customerId = customer.GetProperty("id").GetString();
var scheduleStartDate = customer.GetProperty("creationTime").GetString();What this step answers with
Abridged to the properties this step depends on. A real response carries more.
{
"id": "7b2c4d6e-8f0a-4c3d-9e5f-6a7b8c9d0e1f",
"merchantId": "{{merchantId}}",
"name": "{{customerName}}",
"isActive": true,
"creationTime": "2026-02-04T18:22:41.517Z"
}4. Store the customer's card for the renewals
API call
POST /api/customers/add-stored-payment-method-async?customerId={{customerId}}
Send the card once more, this time to store it. The platform vaults it and answers with the opaque publicReference handle plus masked details you can show the customer, such as "Visa ending 1111" on a billing page. Store the publicReference against your subscriber record and nothing else about the card. Do this only when the customer has agreed you may keep their card for recurring charges, and keep a record of that agreement.
Values this step gives you
{{savedCardToken}}The opaque handle for the stored payment method, from the response body's publicReference property. The contract below is charged against this value.
curl -X POST \
"{{BASE_URL}}/api/customers/add-stored-payment-method-async?customerId={{customerId}}" \
-H "api-key: {{API_KEY}}" \
-H "Content-Type: application/json" \
-d '{
"paymentMethodType": "Card",
"customName": "Subscription card",
"isDefault": true,
"cardData": {
"cardNumber": "4111111111111111",
"nameOnCard": "Jane Doe",
"expirationMonth": 12,
"expirationYear": 2030,
"cvv": 123
}
}'var stored = await http.PostAsJsonAsync(
$"/api/customers/add-stored-payment-method-async?customerId={customerId}",
new
{
paymentMethodType = "Card",
customName = "Subscription card",
isDefault = true,
cardData = new
{
cardNumber = "4111111111111111",
nameOnCard = "Jane Doe",
expirationMonth = 12,
expirationYear = 2030,
cvv = 123
}
});
stored.EnsureSuccessStatusCode();
var method = await stored.Content.ReadFromJsonAsync<JsonElement>();
// The opaque pt_ handle. Store this against your subscriber and nothing else
// about the card.
var savedCardToken = method.GetProperty("publicReference").GetString();What this step answers with
Abridged to the properties this step depends on. A real response carries more.
{
"id": "4e6f8a0b-2c3d-4e5f-9a6b-7c8d9e0f1a2b",
"publicReference": "pt_7Qh2Kd4RmT9xLbVn",
"paymentMethodType": "Card",
"displayName": "Visa ending 1111",
"origin": "Api",
"isDefault": true
}Put them on a schedule
5. Create the contract that carries the schedule
API call
POST /api/contracts
Name the customer, the stored payment method, the amount and the cadence, and the platform owns the timer from here. This is the step that replaces the nightly job in your own service. Send the handle on its own under payMethod. Raw card data sent beside a handle is rejected, and raw card data instead of one puts the number back in your system for every renewal. The amount has to be greater than zero, and the start date can't be in the past.
Values this step gives you
{{contractId}}The id of the created contract, from the response body's id property.
curl -X POST "{{BASE_URL}}/api/contracts" \
-H "api-key: {{API_KEY}}" \
-H "Content-Type: application/json" \
-d '{
"name": "Monthly subscription",
"customerId": "{{customerId}}",
"isActive": true,
"payMethod": {
"type": "Card",
"paymentTokenReference": "{{savedCardToken}}"
},
"perBillInvoice": {
"billSubtotalAmount": 10.00,
"taxAmount": 0.00,
"totalAmount": 10.00
},
"schedule": {
"frequency": "Monthly",
"interval": 1,
"startDate": "{{scheduleStartDate}}"
}
}'var contract = await http.PostAsJsonAsync("/api/contracts", new
{
name = "Monthly subscription",
customerId,
isActive = true,
payMethod = new
{
type = "Card",
paymentTokenReference = savedCardToken
},
perBillInvoice = new
{
billSubtotalAmount = 10.00m,
taxAmount = 0.00m,
totalAmount = 10.00m
},
schedule = new
{
frequency = "Monthly",
interval = 1,
startDate = scheduleStartDate
}
});
contract.EnsureSuccessStatusCode();
var created = await contract.Content.ReadFromJsonAsync<JsonElement>();
var contractId = created.GetProperty("id").GetString();What this step answers with
Abridged to the properties this step depends on. A real response carries more.
{
"id": "2e4f6a8b-0c1d-4e5f-8a9b-0c1d2e3f4a5b",
"customerId": "{{customerId}}",
"name": "Monthly subscription",
"isActive": true,
"paymentMethodType": "Card",
"amountPerAttempt": 10.00,
"totalExecutions": 0,
"payMethod": {
"type": "Card",
"paymentTokenReference": "{{savedCardToken}}"
},
"schedule": {
"frequency": "Monthly",
"interval": 1,
"startDate": "{{scheduleStartDate}}",
"nextScheduledRun": "{{scheduleStartDate}}"
}
}Watch the schedule run
6. Ask an administrator to run the schedule now
API call
POST /api/customers/recurring-billing/trigger
The schedule runs on the platform's own timer, so a live contract needs nothing further from you. To watch a run happen rather than wait for one, an administrator can start one on demand and narrow it to a single contract. This call is gated on an administrative permission, so your integration key can't make it and this platform doesn't run this step for you. Bring it to whoever administers the account.
Values this step gives you
curl -X POST "{{BASE_URL}}/api/customers/recurring-billing/trigger" \
-H "api-key: {{API_KEY}}" \
-H "Content-Type: application/json" \
-d '{
"contractIdFilter": "{{contractId}}",
"reason": "Verifying a new subscription in the sandbox"
}'var run = await http.PostAsJsonAsync(
"/api/customers/recurring-billing/trigger",
new
{
contractIdFilter = contractId,
reason = "Verifying a new subscription in the sandbox"
});
run.EnsureSuccessStatusCode();
var triggered = await run.Content.ReadFromJsonAsync<JsonElement>();
var runId = triggered.GetProperty("runId").GetString();What this step answers with
Abridged to the properties this step depends on. A real response carries more.
{
"runId": "6a8b0c2d-4e5f-4a7b-8c9d-0e1f2a3b4c5d",
"message": "Recurring billing run started."
}7. Read what the run did to this contract
API call
GET /api/customers/recurring-billing/contracts/{{contractId}}/history
Each run comes back with the contracts it touched, what it charged and what it couldn't. Read it to reconcile a billing day rather than inferring the outcome from your own records. This call is gated on the same administrative permission as the trigger above, so it's documented here rather than run for you. For the signal your own integration should act on, subscribe to the transaction webhooks instead: a contract charge raises the same events an ordinary sale does.
Values this step gives you
curl "{{BASE_URL}}/api/customers/recurring-billing/contracts/{{contractId}}/history" \
-H "api-key: {{API_KEY}}"var history = await http.GetFromJsonAsync<JsonElement>(
$"/api/customers/recurring-billing/contracts/{contractId}/history");
foreach (var summary in history.GetProperty("items").EnumerateArray())
{
var status = summary.GetProperty("status").GetString();
var succeeded = summary.GetProperty("successCount").GetInt32();
var failed = summary.GetProperty("failedCount").GetInt32();
}What this step answers with
Abridged to the properties this step depends on. A real response carries more.
{
"items": [
{
"runId": "6a8b0c2d-4e5f-4a7b-8c9d-0e1f2a3b4c5d",
"merchantId": "{{merchantId}}",
"status": "Completed",
"triggerType": "Manual",
"runDate": "2026-02-04T18:22:43.062Z",
"contractCount": 1,
"successCount": 1,
"failedCount": 0,
"skippedCount": 0,
"totalAmount": 10.00
}
],
"pageItemCount": 1,
"nextContinuationToken": null
}8. When the subscription ends
On your side
Cancelling a subscription is two things, and doing only the first is the common mistake. Deactivate the contract so the schedule stops, and retire the stored payment method so the card you were given permission to keep stops being kept. "Save a card and charge it later" covers the retirement end to end, including why deactivating beats deleting once any transaction references the handle.
Save a card and charge it later