Invoice a customer and get paid
Create a customer, write them an invoice, issue it, and record the payment when it arrives, so you know what the platform does at each stage of an invoice's life.
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.
9 steps, 6 API callsInvoicingCustomersPayments
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.
Set up the customer
1. Read your merchant id off a sandbox sale
API call
POST /api/transactions
Every invoicing call names the merchant it bills as, and your key belongs to exactly one. There is no call that answers with that id on its own, so read it off the first merchant-scoped record your key creates: here, a card sale at the amount the sandbox always approves. If you have already run another flow on this site, the merchantId on any of its responses is the same value.
Values this step gives you
{{merchantId}}The merchant the sale belongs to, from the response body's merchantId property. Every invoicing call below bills as this merchant.
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
}2. Create the customer you are invoicing
API call
POST /api/customers
An invoice is addressed to a customer record, so the customer comes first. 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: it's the recipient of the invoice below, and it's also how a customer portal or a payment link knows whose invoice it's showing.
Values this step gives you
{{customerId}}The id of the created customer, from the response body's id property. The invoice names it as its recipient.
curl -X POST "{{BASE_URL}}/api/customers" \
-H "api-key: {{API_KEY}}" \
-H "Content-Type: application/json" \
-d '{
"merchantId": "{{merchantId}}",
"name": "Jane Doe",
"isActive": true,
"contactDetail": {
"primaryContactName": "Jane Doe",
"emailAddress": "jane.doe@example.com",
"phone": "5555550123",
"addresses": [
{
"address1": "100 Main Street",
"city": "Minneapolis",
"state": "MN",
"zip": "55401",
"isDefault": true
}
]
}
}'var created = await http.PostAsJsonAsync("/api/customers", new
{
merchantId,
name = "Jane Doe",
isActive = true,
contactDetail = new
{
primaryContactName = "Jane Doe",
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();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": "Jane Doe",
"isActive": true
}Write the invoice
3. Create the invoice as a draft
API call
POST /api/invoicing/invoices
Send the biller, the recipient, the currency, the terms and the lines. The platform answers with a draft: totals are computed, the status is Draft, and there is no invoice number yet. A draft is the one stage you can still edit, so this is where your own review or approval step belongs. Lines need only a description, a quantity and a unit price; link one to a catalog product when you sell the same thing again and again, and leave productId off when you don't.
Values this step gives you
{{invoiceId}}The id of the draft, from the response body's id property. Every later call addresses the invoice by it; the invoice number is for people and isn't assigned until issue.
curl -X POST "{{BASE_URL}}/api/invoicing/invoices" \
-H "api-key: {{API_KEY}}" \
-H "Content-Type: application/json" \
-d '{
"billerId": "{{merchantId}}",
"billerType": "Merchant",
"recipientId": "{{customerId}}",
"recipientType": "Customer",
"currency": "USD",
"paymentTermsValue": "Net30",
"recipientSnapshot": {
"name": "Jane Doe",
"email": "jane.doe@example.com"
},
"lineItems": [
{ "description": "Website redesign", "quantity": 1, "unitPrice": 1200.00 },
{ "description": "Managed hosting, monthly", "quantity": 12, "unitPrice": 25.00 }
]
}'var drafted = await http.PostAsJsonAsync("/api/invoicing/invoices", new
{
billerId = merchantId,
billerType = "Merchant",
recipientId = customerId,
recipientType = "Customer",
currency = "USD",
paymentTermsValue = "Net30",
recipientSnapshot = new { name = "Jane Doe", email = "jane.doe@example.com" },
lineItems = new[]
{
new { description = "Website redesign", quantity = 1m, unitPrice = 1200.00m },
new { description = "Managed hosting, monthly", quantity = 12m, unitPrice = 25.00m }
}
});
drafted.EnsureSuccessStatusCode();
var draft = await drafted.Content.ReadFromJsonAsync<JsonElement>();
var invoiceId = draft.GetProperty("id").GetString();
var status = draft.GetProperty("status").GetString(); // "Draft"What this step answers with
Abridged to the properties this step depends on. A real response carries more.
{
"id": "8c0d2e4f-6a7b-4c9d-8e1f-2a3b4c5d6e7f",
"billerId": "{{merchantId}}",
"billerType": "Merchant",
"recipientId": "{{customerId}}",
"recipientType": "Customer",
"status": "Draft",
"invoiceNumber": null,
"currency": "USD",
"paymentTermsValue": "Net30",
"issueDate": null,
"dueDate": null,
"subtotal": 1500.00,
"taxTotal": 0.00,
"grandTotal": 1500.00,
"amountPaid": 0.00,
"balanceDue": 1500.00,
"paidAt": null,
"isLocked": false,
"lineItems": [
{
"description": "Website redesign",
"quantity": 1,
"unitPrice": 1200.00,
"lineTotal": 1200.00
},
{
"description": "Managed hosting, monthly",
"quantity": 12,
"unitPrice": 25.00,
"lineTotal": 300.00
}
]
}4. Issue the invoice
API call
POST /api/invoicing/invoices/{{invoiceId}}/issue
Issuing is the point of no return. The platform assigns the invoice number from the biller's sequence, stamps the issue date, computes the due date from the Net 30 terms, freezes the biller and recipient snapshots, and locks the document: the status is Issued and an update from here on is refused. The body is empty; the invoice id in the route is the whole request. Do this when the invoice is final, and not before.
Values this step gives you
curl -X POST "{{BASE_URL}}/api/invoicing/invoices/{{invoiceId}}/issue" \
-H "api-key: {{API_KEY}}"var issuing = await http.PostAsync(
$"/api/invoicing/invoices/{invoiceId}/issue", content: null);
issuing.EnsureSuccessStatusCode();
var issued = await issuing.Content.ReadFromJsonAsync<JsonElement>();
var invoiceNumber = issued.GetProperty("invoiceNumber").GetString();
var balanceDue = issued.GetProperty("balanceDue").GetDecimal();What this step answers with
Abridged to the properties this step depends on. A real response carries more.
{
"id": "{{invoiceId}}",
"billerId": "{{merchantId}}",
"billerType": "Merchant",
"recipientId": "{{customerId}}",
"recipientType": "Customer",
"status": "Issued",
"invoiceNumber": "INV-00001",
"currency": "USD",
"paymentTermsValue": "Net30",
"issueDate": "2026-02-04T18:22:43.062Z",
"dueDate": "2026-03-06T18:22:43.062Z",
"subtotal": 1500.00,
"taxTotal": 0.00,
"grandTotal": 1500.00,
"amountPaid": 0.00,
"balanceDue": 1500.00,
"paidAt": null,
"isLocked": true,
"lineItems": [
{
"description": "Website redesign",
"quantity": 1,
"unitPrice": 1200.00,
"lineTotal": 1200.00
},
{
"description": "Managed hosting, monthly",
"quantity": 12,
"unitPrice": 25.00,
"lineTotal": 300.00
}
]
}Get paid
5. Record the payment
API call
POST /api/invoicing/invoices/{{invoiceId}}/record-payment
When the customer pays outside the platform, by check or by bank transfer, tell the invoice about it. Send the amount received and how it arrived. The platform applies it to the balance and moves the status: to Paid when the balance reaches zero, as here, or to PartiallyPaid when it doesn't. A partial amount is refused unless the invoice allows partial payment, and any amount above the balance is refused outright. A payment the customer makes through a payment link is applied for you by the same rule, so your reconciliation reads one status whichever way the money came.
Values this step gives you
curl -X POST "{{BASE_URL}}/api/invoicing/invoices/{{invoiceId}}/record-payment" \
-H "api-key: {{API_KEY}}" \
-H "Content-Type: application/json" \
-d '{
"amount": 1500.00,
"paymentMethod": "Check",
"notes": "Check received by mail."
}'var recording = await http.PostAsJsonAsync(
$"/api/invoicing/invoices/{invoiceId}/record-payment",
new
{
amount = 1500.00m,
paymentMethod = "Check",
notes = "Check received by mail."
});
recording.EnsureSuccessStatusCode();
var paid = await recording.Content.ReadFromJsonAsync<JsonElement>();
var paidStatus = paid.GetProperty("status").GetString(); // "Paid"What this step answers with
Abridged to the properties this step depends on. A real response carries more.
{
"id": "{{invoiceId}}",
"billerId": "{{merchantId}}",
"billerType": "Merchant",
"recipientId": "{{customerId}}",
"recipientType": "Customer",
"status": "Paid",
"invoiceNumber": "INV-00001",
"currency": "USD",
"paymentTermsValue": "Net30",
"issueDate": "2026-02-04T18:22:43.062Z",
"dueDate": "2026-03-06T18:22:43.062Z",
"subtotal": 1500.00,
"taxTotal": 0.00,
"grandTotal": 1500.00,
"amountPaid": 1500.00,
"balanceDue": 0.00,
"paidAt": "2026-02-04T18:22:43.062Z",
"isLocked": true,
"lineItems": [
{
"description": "Website redesign",
"quantity": 1,
"unitPrice": 1200.00,
"lineTotal": 1200.00
},
{
"description": "Managed hosting, monthly",
"quantity": 12,
"unitPrice": 25.00,
"lineTotal": 300.00
}
]
}6. Read the invoice back
API call
GET /api/invoicing/invoices/{{invoiceId}}
Read the invoice you just settled. The status is Paid, the balance due is zero, the amount paid equals the grand total, and paidAt records when the balance cleared. Reconcile against this record rather than against the response you got from recording the payment, so a request that timed out on your side still has somewhere to recover the outcome from.
Values this step gives you
curl "{{BASE_URL}}/api/invoicing/invoices/{{invoiceId}}" \
-H "api-key: {{API_KEY}}"var invoice = await http.GetFromJsonAsync<JsonElement>(
$"/api/invoicing/invoices/{invoiceId}");
var settled = invoice.GetProperty("status").GetString() == "Paid"
&& invoice.GetProperty("balanceDue").GetDecimal() == 0m;
var paidAt = invoice.GetProperty("paidAt").GetString();What this step answers with
Abridged to the properties this step depends on. A real response carries more.
{
"id": "{{invoiceId}}",
"billerId": "{{merchantId}}",
"billerType": "Merchant",
"recipientId": "{{customerId}}",
"recipientType": "Customer",
"status": "Paid",
"invoiceNumber": "INV-00001",
"currency": "USD",
"paymentTermsValue": "Net30",
"issueDate": "2026-02-04T18:22:43.062Z",
"dueDate": "2026-03-06T18:22:43.062Z",
"subtotal": 1500.00,
"taxTotal": 0.00,
"grandTotal": 1500.00,
"amountPaid": 1500.00,
"balanceDue": 0.00,
"paidAt": "2026-02-04T18:22:43.062Z",
"isLocked": true,
"lineItems": [
{
"description": "Website redesign",
"quantity": 1,
"unitPrice": 1200.00,
"lineTotal": 1200.00
},
{
"description": "Managed hosting, monthly",
"quantity": 12,
"unitPrice": 25.00,
"lineTotal": 300.00
}
]
}Go further
7. Send the invoice to the customer
On your side
In production you send the invoice rather than recording a payment by hand. The send call runs the whole delivery workflow: it issues the invoice if it's still a draft, creates a payment link and a view token for the recipient, marks the status Sent, and delivers the notification. That last part needs a merchant with a configured email destination, which a fresh sandbox may not have, and it's why this flow records a payment as its runnable path instead. Once you have configured delivery, call send from your own integration and let the payment link do the collecting.
Values this step gives you
8. Download the invoice as a PDF
On your side
Any issued invoice can be rendered as a PDF from the snapshots and lines frozen at issue. Fetch it when your customer asks for a copy or when your own records need the document rather than the data. The response is the file itself, not JSON.
Values this step gives you
9. Offer a payment plan
On your side
A larger invoice can be split into scheduled installments the platform collects against a stored payment method. Create the plan on an issued invoice; each installment is applied to the balance the same way the payment above was, so the invoice reaches Paid when the last one clears. Payment plans, credit notes and recurring invoices each have their own reference pages.