Invoices#
Create, send, and manage invoices with built-in partial payment support.
Overview#
The Invoices API enables you to:
- 📄 Create invoices - Line items, tax and discounts, totals calculated server-side
- 📧 Send via email/SMS - Deliver invoices to customers
- 💰 Accept payments - Every invoice gets its own hosted pay link
- 📊 Partial payments - Optional, with an optional minimum payment rule
- 📱 QR codes - Scannable code for the pay link
- 📋 PDF generation - Downloadable invoice PDFs
- 🔔 Reminders - Manual reminder emails
Authentication#
All /api/v1/invoices endpoints are admin REST endpoints:
Authorization: Bearer YOUR_TOKEN
X-Tenant-ID: your-tenant-id
Content-Type: application/jsonX-Tenant-ID is required and must match the tenant in the token (401 otherwise). A server-side API key sent as x-api-key is also accepted; the gateway resolves the tenant (and, for a merchant-scoped key, the merchant) from the key. Endpoints that check a user permission (external-payment) reject API-key calls with 403 FORBIDDEN.
Invoices have no expiry#
An invoice has an issue date and a due date only. It does not expire: past its due date it becomes overdue and stays payable. (Standalone payment links are different and do expire.) Do not send expiresAt / expiresInDays on invoices.
Location#
invoiceLocationType is the first-class sales-channel field. It accepts physical, online, ornull for legacy callers and rows. A physical Invoice Location uses its own active branch UUID as
the Invoice-level stockLocationId. The Invoice-level field is also retained for the existing
Online Send/Edit reservation contract and bounded legacy-client compatibility.
The portal also retains metadata.location as a legacy display/filter name (for example"Headquarters"). It is not an identifier and is never used to choose fulfillment. The server
does not infer a physical branch from metadata.location, a merchant id, a default branch, a
branch name, or the word Online.
Stock source for tracked products#
An Online tracked Product line may persist its own optional writablelineItems[].stockLocationId. The value must identify an active physical branch owned by the
Invoice merchant when newly supplied. Saving a Draft may omit it and has no stock effect. Services,
free-text lines, and untracked Products do not retain a line stock location. Changing a line's
Product clears its prior branch selection.
The per-line field currently provides a durable round-trip and the input to
Inventory Preflight. It does not by itself replace the Invoice-level
location used by the existing Send/Edit reservation mutation. A physical Invoice still uses its
Invoice-level branch as the effective source; any hidden Online per-line choices remain persisted
for a later switch back.
The server reserves tracked quantities when an Invoice enters sent. With zero-stock selling
disabled, insufficient Available stock rejects Send or the relevant pre-payment edit with409 INVOICE_INVENTORY_INSUFFICIENT; error.details.violations supplies the Product, requested
quantity, server-calculated Available quantity, and physical location. With zero-stock selling
enabled, the server reserves only Available quantity and records the unreserved shortfall. Clients
must render the returned error/detail state and must not calculate availability themselves.
Once the first payment is recorded, the fulfillment location is locked. Later stock changes never
reject or reverse a successful payment. On the first fully-paid transition, the server consumes the
Invoice's own reservation, deducts only stock that can still be deducted without making inventory
negative, and records any remainder as not deducted for Pending Review.
Additive location and detail contract#
The location request change is additive. Before this contract, callers could provide only the
legacy display name:
{
"metadata": { "location": "Headquarters" }
}After this contract, invoiceLocationType is added while the top-level stockLocationId continues
to carry the Invoice-level physical fulfillment ID:
{
"invoiceLocationType": "physical",
"stockLocationId": "2d8b4e17-ec2e-41f2-b6e9-6a32c5fd1780",
"metadata": { "location": "Headquarters" }
}For an Online sales channel, each tracked Product line can separately persist a branch selection:
{
"invoiceLocationType": "online",
"lineItems": [
{
"id": "li_1",
"stableLineId": "li_1",
"productId": "c126128b-7447-414f-80a9-61582fd8a608",
"quantity": 5,
"stockLocationId": "2d8b4e17-ec2e-41f2-b6e9-6a32c5fd1780"
}
],
"metadata": { "location": "Online" }
}Existing callers may continue omitting invoiceLocationType, subject to the Send rules above.
Before this contract, responses had no first-class invoiceLocationType or server-derived detail
block. Create and write responses now include the persisted location fields below
(stockLocationId remains the physical branch field):
{
"invoiceLocationType": "online",
"stockLocationId": "2d8b4e17-ec2e-41f2-b6e9-6a32c5fd1780",
"stockLocationName": "Warehouse East",
"inventoryVersion": 0
}GET /api/v1/invoices/:invoiceId additionally returns server-derived fulfillment lock,
reservation, and inventory Activity fields. See Get Invoice for the exact shape.
Writable and response-only line location fields#
Only lineItems[].stockLocationId is writable stock-location data. The other line location fields
are server-owned response projections and must not be copied into a write request as authority.
| Line field | Direction | Meaning |
|---|---|---|
stockLocationId |
Request and response | Persisted optional branch selection for the line. A legacy/effective fallback is never written into this field by a read. |
stockLocationName |
Response only | Current server-resolved name for the persisted selection, or null. |
effectiveStockLocationId |
Response only | Effective persisted-line, Physical, active-reservation, or legacy source without backfilling stockLocationId. |
effectiveStockLocationName |
Response only | Current server-resolved name for the effective source, or null. |
stockLocationSource |
Response only | persisted_line, physical_invoice, active_reservation, legacy_invoice, or null. |
reservedQuantity |
Response only | Quantity held by the stable Product line's active reservation, independently of the effective source; otherwise 0. It is not writable availability. |
Client-supplied names, effective fields, provenance, reservation quantities, and availability are
discarded or overwritten. An old-client edit that omits line locations preserves existing values
only when stable line and Product identity match unambiguously; conflicting mixed Invoice-level
and per-line Online sources are rejected.
Unauthenticated public checkout responses omit the top-level and per-line internal branch IDs,
branch names, effective fields, provenance, and reservation quantities.
Inventory Preflight#
Evaluate the complete candidate Invoice against current branch availability without saving the
Invoice or changing inventory.
Endpoint#
Without invoiceId, the caller needs invoices.create; merchant scope is resolved the same way as
Create. With invoiceId, the caller needs invoices.manage; the server loads the Invoice inside
the authenticated tenant/merchant scope. Do not send merchantId in the body. An inaccessible Edit
Invoice returns the normal non-disclosing 404 NOT_FOUND, while a permission failure returns403 FORBIDDEN.
Request#
{
"invoiceLocationType": "online",
"lineItems": [
{
"lineId": "li_1",
"productId": "c126128b-7447-414f-80a9-61582fd8a608",
"quantity": 8,
"stockLocationId": "2d8b4e17-ec2e-41f2-b6e9-6a32c5fd1780"
}
]
}| Field | Type | Required | Description |
|---|---|---|---|
invoiceId |
uuid | No | Include only for Edit. The server derives the merchant from this scoped Invoice. |
invoiceLocationType |
string | Yes | Exact lowercase online or physical. |
invoiceStockLocationId |
uuid | null | No | Candidate effective branch for physical; ignored as a per-line Online selection. A missing required selection is reported in the evaluation. |
lineItems |
array | Yes | Complete candidate Draft, maximum 200 lines. |
lineItems[].lineId |
string | Yes | Nonblank stable identity, unique within the request. |
lineItems[].productId |
uuid | null | No | Current Product identity. Product facts are loaded server-side. |
lineItems[].quantity |
number | Yes | Greater than 0, at most 1000000, and at most 3 decimal places. |
lineItems[].stockLocationId |
uuid | null | No | Candidate Online branch selection. |
Unknown top-level or line fields are rejected with 400 VALIDATION_ERROR, including attempted
client authority such as availability, policy, tracking state, branch names, tenant, or merchant.
Blank/duplicate line IDs, malformed quantities, more than 200 lines, and Products unavailable in
the resolved merchant scope also return the normal validation envelope.
Response#
200 OK returns { "success": true, "data": { "lines": [], "blockingErrors": [], "advisories": [] } } even when blockingErrors is nonempty:
| Response field | Meaning |
|---|---|
data.lines[] |
Evaluated Product-backed candidate lines. Free-text lines are omitted. |
lineId, productId, selectedLocationId, allowZeroStockSale |
Stable identity, server-resolved Product policy, and candidate effective selection. |
branches[] |
Every active physical branch, with locationId, locationName, availableQuantity, reservedQuantity, availableAfterOtherDraftLines, selectable, and reason (available or insufficient). |
expectedReservedQuantity, shortfallQuantity |
Deterministic preview allocation for the selected branch; neither value is committed stock. |
lines[].blockingErrors, lines[].advisories |
Issues affecting that line. |
data.blockingErrors, data.advisories |
Whole-candidate issues, grouped by Product and branch. |
Each issue contains type (missing_location, inactive_location, or insufficient_stock),severity (error or warning), lineIds, productId, productName, nullable locationId andlocationName, requestedQuantity, availableQuantity, and reservedQuantity.
Every active physical branch is returned, including branches with zero balance. For Edit,availableQuantity adds back only that Invoice's own active holds before evaluating the candidate;
other Invoices' reservations remain unavailable. Duplicate Product lines sharing a branch are
grouped for issues and allocated in stable lineId order, so expectedReservedQuantity,shortfallQuantity, and availableAfterOtherDraftLines are deterministic.
missing_location, inactive_location, and insufficient stock with zero-stock selling disabled
are blocking errors. Insufficient stock with zero-stock selling enabled is a warning advisory;
short branches remain selectable and the expected quantity is bounded by current Available.
Issues include affected lineIds plus Product, branch, Requested, Available, and Reserved facts.
Preflight is advisory, not a reservation or approval. It performs no Invoice, balance,
reservation, or Activity write, and its expectedReservedQuantity is not committed stock. A later
Create, Send, or Edit remains authoritative and may observe newer inventory.
Create an Invoice#
Endpoint#
Request#
curl -X POST https://uat.auxcore.net/api/v1/invoices \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "X-Tenant-ID: your-tenant-id" \
-H "Content-Type: application/json" \
-d '{
"merchantId": "6f1c2a9e-3b7d-4e21-9a55-0c8f1d2b7e40",
"customerName": "John Doe",
"customerEmail": "john@example.com",
"customerPhone": "5551234567",
"lineItems": [
{
"id": "li_1",
"name": "Web Design Services",
"description": "Homepage redesign",
"quantity": 10,
"unit": "hour",
"unitPrice": 150.00,
"taxRate": 8
},
{
"id": "li_2",
"name": "Hosting (Annual)",
"quantity": 1,
"unitPrice": 299.00,
"discount": 10,
"discountType": "percent"
}
],
"issueDate": "2026-10-01",
"dueDate": "2026-10-15",
"discountAmount": 29.90,
"allowPartialPayment": true,
"minimumPaymentType": "percent",
"minimumPaymentValue": 25,
"notes": "Payment due within 15 days",
"poNumber": "PO-456",
"invoiceLocationType": "physical",
"stockLocationId": "2d8b4e17-ec2e-41f2-b6e9-6a32c5fd1780",
"metadata": { "location": "Headquarters" }
}'Body Parameters#
| Parameter | Type | Required | Description |
|---|---|---|---|
merchantId |
uuid | Yes | Merchant issuing the invoice. Filled from the key for a merchant-scoped API key |
lineItems |
array | Yes* | Line items (see below). *Or send amount instead |
amount |
number | Yes* | Manual amount when there are no line items; becomes a single "Payment" line |
customerName / customerEmail / customerPhone |
string | No | Customer contact. A Customer Vault record is found or created from these |
customerId |
uuid | No | Existing Customer Vault id, to link the invoice to that customer explicitly |
billingAddress, billingCity, billingState, billingPostalCode, billingCountry |
string | No | Billing address |
type |
string | No | standard (default), recurring, text_to_pay, email_to_pay |
status |
string | No | Initial status. Defaults to sent; send draft to save without issuing |
discountAmount |
number | No | Total discount in dollars (line discounts plus any invoice-level discount) |
taxRate |
number | No | Stored for display; tax is calculated from each line's taxRate |
allowPartialPayment |
boolean | No | Default false |
minimumPaymentType |
string | No | amount or percent (only with partial payments) |
minimumPaymentValue |
number | No | percent: greater than 0 and less than 100. amount: at least 0.50 and less than the total |
nextPaymentAfterDays |
integer | No | Days after issue date for the next automatic instalment (partial payments only); must not land after dueDate |
allowRescheduleNextPayment |
boolean | No | Let the payer move the next instalment date (partial payments only) |
issueDate |
date | No | YYYY-MM-DD. Defaults to today |
dueDate |
date | No | YYYY-MM-DD |
reminderDaysBefore |
integer | No | Default 0 |
notes, terms, poNumber, reference |
string | No | Free text |
webhookUrl |
string | No | URL that receives invoice.paid for this invoice |
invoiceLocationType |
string | null | No | physical or online. null/omitted is retained for legacy callers and is never inferred from metadata |
stockLocationId |
uuid | null | Conditional | Validated active physical fulfillment branch. Required for physical; required before issuing an Online Invoice with tracked Product lines |
metadata |
object | No | Free-form. Recognised keys: location (location name), acceptCards / acceptAch (default true; restrict pay methods), recurringSchedule (recurring invoices) |
autoSend |
boolean | No | When the invoice is not a draft and has an email, it is emailed after creation. Send false to deliver it yourself |
Line Item Fields#
| Field | Type | Description |
|---|---|---|
id |
string | Your line id |
stableLineId |
string | Optional durable line identity. Keep it stable across Edit and preflight requests. |
productId |
uuid | null | Catalog Product identity. Omit for a free-text line. |
stockLocationId |
uuid | null | Persisted writable physical-branch selection for an Online tracked Product line. Optional on Draft; ignored/cleared for services, free-text lines, and untracked Products. |
name |
string | Line name |
description |
string | Line description |
quantity |
number | Defaults to 1. Greater than 0, up to 3 decimal places (2.5); otherwise 400 VALIDATION_ERROR |
unit |
string | One of piece, each, box, hour, day, kg, lb, ton, liter, gallon, metre, foot, mile, mil, other. Optional. Common spellings are accepted and stored as the code (gallons → gallon, meter → metre, lbs → lb); an unknown unit is 400 VALIDATION_ERROR |
unitPrice |
number | Price per unit |
discount |
number | Line discount |
discountType |
string | fixed (default) or percent |
taxRate |
number | Line tax percentage (e.g. 8 = 8%) |
freightAmount |
number | Per-line freight charge. It is included in the line amount and invoice total. |
See Writable and response-only line location fields
for the server-derived fields that can appear when the Invoice is read.
amount, taxAmount, subtotal and totalAmount are always calculated by the server; any values you send are overwritten. Totals: totalAmount = subtotal − discountAmount + taxAmount + freightAmount + convenienceFee, where subtotal is quantity × unit price before any discount.
Line discounts must also be counted in
discountAmount. A line'sdiscountlowers that line's taxable base, but the invoice total only subtracts the invoice-leveldiscountAmount. SenddiscountAmountas the sum of all line discounts plus any invoice-level discount (the example above: 10% of 299.00 = 29.90), otherwise the line discount is not taken off the total.
Response#
201 Created with the invoice:
{
"success": true,
"data": {
"id": "0b8e6a3c-5d2f-4c71-9e1a-7f3b2c9d4e10",
"invoiceNumber": "INV-2026-000123",
"displayInvoiceNumber": "INV-2026-000123",
"linkId": "INV-1790812800000-A1B2C3D4",
"shortLink": "https://uat.auxcore.net/s/Ab12Cd",
"merchantId": "6f1c2a9e-3b7d-4e21-9a55-0c8f1d2b7e40",
"type": "standard",
"status": "sent",
"customerId": "3d7f0c1e-8a2b-4f6d-b9c3-1e5a7d2f8b04",
"customerName": "John Doe",
"customerEmail": "john@example.com",
"lineItems": [ ... ],
"subtotal": 1799.00,
"discountAmount": 29.90,
"taxAmount": 120.00,
"convenienceFee": 0,
"totalAmount": 1889.10,
"amountPaid": 0,
"amountDue": 1889.10,
"currency": "USD",
"allowPartialPayment": true,
"minimumPaymentType": "percent",
"minimumPaymentValue": 25,
"issueDate": "2026-10-01",
"dueDate": "2026-10-15",
"invoiceLocationType": "physical",
"stockLocationId": "2d8b4e17-ec2e-41f2-b6e9-6a32c5fd1780",
"stockLocationName": "Headquarters",
"inventoryVersion": 0,
"metadata": { "location": "Headquarters" },
"createdAt": "2026-10-01T09:00:00.000Z"
}
}Money fields (subtotal, discountAmount, taxAmount, convenienceFee, totalAmount, amountPaid, amountDue, minimumPaymentValue) are numbers in this create response but decimal strings (e.g. "1889.10") whenever the invoice is read back (get, list, lookup); write responses can mix both (e.g. after a payment amountPaid / amountDue are numbers while totalAmount is a string). Parse them before doing arithmetic. Amounts inside paymentHistory entries are numbers.
The customer pays at shortLink, or at {host}/pay/invoice/{linkId} when no short link was minted.
Errors#
| Status | Code | When |
|---|---|---|
400 |
VALIDATION_ERROR |
Missing merchantId, no line items and no amount, or an invalid minimum payment / next-payment rule |
400 |
VALIDATION_ERROR |
Invalid invoiceLocationType, a physical Invoice Location without its active branch id, or an issued tracked Invoice without a Fulfillment location |
400 |
QUANTITY_LIMIT_EXCEEDED |
A catalog product's quantity, summed across the lines with its productId, is over its maxQuantity. error.details.violations lists each one — see Products, "Per-order quantity limit" |
403 |
MERCHANT_CLOSED / MERCHANT_RESTRICTED |
Merchant cannot issue invoices |
409 |
INVOICE_INVENTORY_INSUFFICIENT |
Send-time reservation failed with zero-stock selling disabled; error.details.violations contains Product and physical-location availability details |
422 |
LIMIT_EXCEEDED |
Total exceeds the merchant's per-transaction limit |
Send an Invoice#
Send invoice via email or SMS. See Sending & Delivery for all delivery endpoints.
Endpoint#
curl -X POST https://uat.auxcore.net/api/v1/invoices/0b8e6a3c-5d2f-4c71-9e1a-7f3b2c9d4e10/send \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "X-Tenant-ID: your-tenant-id" \
-H "Content-Type: application/json" \
-d '{ "method": "email", "to": "john@example.com" }'method (email or sms) and to are both required, but to is only checked for presence: the invoice is always delivered to its stored customerEmail / customerPhone. To send to a different address, edit the invoice first.
When a draft first enters sent, the server validates the explicit physical fulfillment location
and reserves each tracked Product line atomically before delivery. A failed validation or
insufficient-stock result leaves the Invoice draft, balances, reservations, and Activity
unchanged. A later notification-delivery failure does not undo a reservation that the server has
already committed.
An insufficient-stock Send returns this structured error when zero-stock selling is disabled:
{
"success": false,
"error": {
"code": "INVOICE_INVENTORY_INSUFFICIENT",
"message": "Inventory is not available for this invoice.",
"details": {
"violations": [
{
"productId": "c126128b-7447-414f-80a9-61582fd8a608",
"productName": "Widget",
"requestedQuantity": 5,
"availableQuantity": 3,
"locationId": "2d8b4e17-ec2e-41f2-b6e9-6a32c5fd1780",
"locationName": "Warehouse East"
}
]
}
}
}Record a Payment#
Record money that has already been collected against an invoice (full or partial). This endpoint does not charge a card — see Invoice Payments.
Endpoint#
curl -X POST https://uat.auxcore.net/api/v1/invoices/0b8e6a3c-5d2f-4c71-9e1a-7f3b2c9d4e10/payment \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "X-Tenant-ID: your-tenant-id" \
-H "Content-Type: application/json" \
-d '{
"amount": 500.00,
"paymentMethod": "card",
"transactionId": "TXN-20261001-8F3A2C",
"notes": "First installment"
}'Response: the updated invoice, e.g. "status": "partial", "amountPaid": 500, "amountDue": 1389.1, with the payment appended to paymentHistory.
Invoice Statuses#
| Status | Description |
|---|---|
draft |
Saved, not issued. Can be updated or deleted |
sent |
Issued to the customer |
viewed |
Customer opened the pay link |
partial |
Partially paid |
paid |
Fully paid |
overdue |
Past due date and unpaid — still payable |
pending |
An ACH payment is processing |
cancelled |
Voided |
expired |
Legacy only — invoices created before expiry was removed |
sent, viewed, partial, and overdue Invoices retain their active inventory reservation.
Becoming overdue does not release stock. Cancellation/void releases active reservations; payment
conversion consumes them.
Get Invoice#
Endpoint#
curl https://uat.auxcore.net/api/v1/invoices/0b8e6a3c-5d2f-4c71-9e1a-7f3b2c9d4e10 \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "X-Tenant-ID: your-tenant-id"Response: the full invoice object (same shape as create), including paymentHistory,deliveryHistory, editHistory, viewCount, sentAt, viewedAt, paidAt, and these
server-derived detail additions:
fulfillmentLocationLockedandfulfillmentLocationLockReason(first_paymentornull)inventoryReservation.state:none,active,partial,converted_to_sale, orreleasedinventoryReservation.lines[]: Product, physical location, requested/reserved/unreserved
quantities, persisted line state, andupdatedAtinventoryActivity[]: server-recorded reservation and settlement quantities; clients must not
infer Activity from balance totals- Historical rows always include
locationId.locationNameis nullable when the row belongs to
an earlier fulfillment location whose name is not available from the Invoice's current snapshot;
clients must not display the UUID as though it were a branch name.
{
"success": true,
"data": {
"id": "0b8e6a3c-5d2f-4c71-9e1a-7f3b2c9d4e10",
"invoiceNumber": "INV-2026-000123",
"status": "partial",
"totalAmount": "1889.10",
"amountPaid": "500.00",
"amountDue": "1389.10",
"invoiceLocationType": "online",
"stockLocationId": "2d8b4e17-ec2e-41f2-b6e9-6a32c5fd1780",
"stockLocationName": "Warehouse East",
"inventoryVersion": 0,
"fulfillmentLocationLocked": true,
"fulfillmentLocationLockReason": "first_payment",
"inventoryReservation": {
"state": "active",
"lines": [
{
"lineId": "li_1",
"productId": "c126128b-7447-414f-80a9-61582fd8a608",
"productName": "Widget",
"locationId": "2d8b4e17-ec2e-41f2-b6e9-6a32c5fd1780",
"locationName": "Warehouse East",
"requestedQuantity": 5,
"reservedQuantity": 5,
"unreservedQuantity": 0,
"state": "active",
"updatedAt": "2026-10-01T09:01:00.000Z"
}
]
},
"inventoryActivity": [
{
"id": "c983fef5-37d8-4a2e-a766-c034220f3001",
"type": "reservation_created",
"productId": "c126128b-7447-414f-80a9-61582fd8a608",
"locationId": "2d8b4e17-ec2e-41f2-b6e9-6a32c5fd1780",
"locationName": "Warehouse East",
"requestedQuantity": 5,
"reservedQuantity": 5,
"deductedQuantity": null,
"notDeductedQuantity": 0,
"reservationDelta": 5,
"reservedAfter": 5,
"createdAt": "2026-10-01T09:01:00.000Z"
}
],
"paymentHistory": [
{
"amount": 500.00,
"paymentMethod": "card",
"paidAt": "2026-10-02T17:00:00.000Z",
"transactionId": "TXN-20261001-8F3A2C",
"isExternal": false,
"balanceAfter": 1389.10
}
],
"dueDate": "2026-10-15"
}
}Returns 404 NOT_FOUND when the invoice does not exist in your tenant.
Look Up an Invoice#
Find one invoice by any reference — invoice number, external (Aux365 / QuickBooks) number or id, pay link id, PO number, or customer contact.
Endpoint#
| Parameter | Description |
|---|---|
value (or q) |
Value to match |
field |
Restrict to one field: invoiceId, invoiceNumber, aux365InvoiceNumber, quickbooksDocNumber, aux365InvoiceId, quickbooksInvoiceId, linkId, poNumber, customerName, customerEmail, customerPhone |
merchantId |
Required for platform admins; a merchant user may only pass their own |
The field names above can also be passed directly as query parameters (e.g. ?invoiceNumber=INV-2026-000123).
Response: { "success": true, "data": { ...invoice }, "matchedOn": "invoiceNumber", "results": [ ... ] }. Without field, an inexact value falls back to a partial search and results lists every match. The fallback search only covers collectable invoices. Errors: 404 INVOICE_NOT_FOUND when nothing matches, 400 MERCHANT_ID_REQUIRED when a platform admin omits merchantId, 403 MERCHANT_SCOPE_VIOLATION when a merchant user asks for another merchant.
List Invoices#
Endpoint#
Query Parameters#
| Parameter | Type | Description |
|---|---|---|
page |
integer | Page number (default 1) |
limit |
integer | Items per page (default 50) |
merchantId |
uuid | Filter by merchant |
status |
string | Filter by one status |
statuses |
string | Comma-separated statuses (overrides status) |
type |
string | standard, recurring, text_to_pay, email_to_pay |
customerId |
uuid | Filter by Customer Vault id |
customerEmail |
string | Partial, case-insensitive match |
search |
string | Matches customer name, email, invoice number or link id; a money value also matches the total |
fromDate / toDate |
date | Filter on creation date |
delivery |
string | email, text, or link |
location |
string | Invoice Location display name. First-class physical rows match stockLocationName and retain legacy metadata.location / metadata.locationName matching; unclassified legacy rows use metadata fallback. Online matches first-class Online rows and legacy rows with no physical display location |
sort |
string | status to group by status; otherwise newest first |
curl "https://uat.auxcore.net/api/v1/invoices?status=overdue&limit=25" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "X-Tenant-ID: your-tenant-id"Response:
{
"success": true,
"data": [ { "id": "0b8e6a3c-...", "invoiceNumber": "INV-2026-000123", "status": "overdue" } ],
"meta": {
"page": 1,
"limit": 25,
"total": 1,
"statusCounts": { "all": 42, "sent": 10, "overdue": 1, "paid": 31 }
}
}Invoice Summary#
KPIs and status counts for the invoices dashboard.
Endpoint#
Optional query: merchantId.
{
"success": true,
"data": {
"statusCounts": { "all": 42, "sent": 10, "overdue": 1, "paid": 31 },
"outstandingTotal": 12500.00,
"outstandingCount": 11,
"overdueTotal": 1889.10,
"overdueCount": 1,
"paidMonthTotal": 8200.00,
"paidMonthCount": 9,
"avgTimeToPayDays": 6.4,
"avgTimeToPaySampleSize": 31
}
}Update Invoice#
Update a draft invoice. Any other status returns 400 VALIDATION_ERROR ("Only draft invoices can be updated") — to change an invoice that has already been sent, viewed or partially paid, use Edit Invoice below, which rotates the payment link and re-sends the updated invoice.
Endpoint#
curl -X PUT https://uat.auxcore.net/api/v1/invoices/0b8e6a3c-5d2f-4c71-9e1a-7f3b2c9d4e10 \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "X-Tenant-ID: your-tenant-id" \
-H "Content-Type: application/json" \
-d '{
"lineItems": [ ... ],
"notes": "Updated payment terms"
}'Takes the create fields. When lineItems is sent, totals are recalculated, and a product whose
total quantity rises above its maxQuantity is refused with 400 QUANTITY_LIMIT_EXCEEDED (a
quantity already on the invoice before the limit was set or lowered is kept).
Saving a draft still has no inventory effect. If invoiceLocationType is physical,stockLocationId must be that Invoice Location's active branch id even while draft.
Edit Invoice#
Edit an existing invoice in place — the same invoice id and invoice number are returned. The behaviour depends on the invoice's current status, which is re-checked in the API against the latest database row at the moment of the call:
draft/sent/viewed→ full edit. The payment link is rotated: the oldlinkIdis invalidated (it now serves a "link no longer available" message) and a newlinkId/shortLinkis issued. Status is set tosentand the invoice is auto-resent via Email (when it has an email) and SMS (when it has a phone). Channels are chosen by the system — there is no channel picker.- Full edit with
"resend": false→ the invoice is updated in place; the link, status and balance are kept and nothing is sent. partial→ due date, notes andallowRescheduleNextPaymentonly. The existing link is kept, amounts and payment history are untouched, and nothing is resent. Any amount-affecting field in the body (lineItems,taxRate,discountAmount,convenienceFee,currency,totalAmount,amount,allowPartialPayment,minimumPaymentType,minimumPaymentValue) is rejected with400.paid→409 CONFLICT.cancelled/expired/overdue/pending→400 VALIDATION_ERROR.
Endpoint#
Full edit (draft / sent / viewed)#
curl -X POST https://uat.auxcore.net/api/v1/invoices/0b8e6a3c-5d2f-4c71-9e1a-7f3b2c9d4e10/edit \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "X-Tenant-ID: your-tenant-id" \
-H "Content-Type: application/json" \
-d '{
"customerName": "Acme Corp",
"customerEmail": "ap@acme.example",
"customerPhone": "+15551234567",
"lineItems": [
{ "id": "li_1", "name": "Consulting", "quantity": 2, "unitPrice": 125.00, "taxRate": 8.5 }
],
"discountAmount": 0,
"allowPartialPayment": true,
"dueDate": "2026-10-31",
"notes": "Updated scope"
}'Fields you omit keep their current value; metadata is merged into the existing metadata.
Before the first payment, changing tracked lines or stockLocationId on an issued Invoice causes
the server to reconcile the existing reservation atomically. After the first payment,stockLocationId and invoiceLocationType are locked; a change returns409 FULFILLMENT_LOCATION_LOCKED with error.details.reason: "first_payment".
Response (200): the same invoice with a new linkId, the rotated-out id in previousLinkId, and a meta.delivery block reporting the resend outcome (sent, failed or skipped per channel).
{
"success": true,
"data": {
"id": "0b8e6a3c-5d2f-4c71-9e1a-7f3b2c9d4e10",
"invoiceNumber": "INV-2026-000123",
"status": "sent",
"linkId": "INV-1790899200000-NEW12345",
"shortLink": "https://uat.auxcore.net/s/Xy98Zw",
"previousLinkId": "INV-1790812800000-A1B2C3D4",
"totalAmount": "271.25",
"amountPaid": "0.00",
"amountDue": "271.25",
"dueDate": "2026-10-31"
},
"meta": { "delivery": { "email": "sent", "sms": "sent" } }
}
shortLinkmay be missing if the shortener was unavailable; fall back to{host}/pay/invoice/{linkId}. If a resend fails the invoice and the new link stay active (meta.deliveryreportsfailed); callsend-email/send-smsto retry delivery.
Partial edit (due date + notes only)#
curl -X POST https://uat.auxcore.net/api/v1/invoices/0b8e6a3c-5d2f-4c71-9e1a-7f3b2c9d4e10/edit \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "X-Tenant-ID: your-tenant-id" \
-H "Content-Type: application/json" \
-d '{ "dueDate": "2026-11-15", "notes": "Extended terms" }'Response (200): the same invoice in place — linkId, amounts and payment history are all unchanged; only dueDate (and optionally notes) change. No resend is performed.
Old payment link after a full edit#
Once a full edit rotates the link, the old pay URL (/pay/invoice/{oldLinkId}) resolves to:
{
"success": false,
"error": {
"code": "LINK_ROTATED",
"message": "This invoice link is no longer available. Please use the latest invoice link."
}
}returned with HTTP 410 Gone. A link that was never issued still returns 404.
Errors#
| Status | When |
|---|---|
400 |
Amount-affecting field on a partial invoice, a non-editable status (cancelled/expired/overdue/pending), or an invalid minimum payment / next-payment rule |
400 |
QUANTITY_LIMIT_EXCEEDED — the edit raises a product's quantity above its maxQuantity. Quantities already on the invoice before the limit was set or lowered are kept |
403 |
Full edit blocked by merchant status (MERCHANT_CLOSED / MERCHANT_RESTRICTED) |
404 |
Invoice not found for this tenant |
409 |
Invoice is paid, or became paid / partial before a full edit was saved |
422 |
Full-edit total exceeds the merchant per-transaction limit (LIMIT_EXCEEDED) |
Cancel Invoice#
Void an issued invoice. Only issued, unpaid invoices (sent, viewed) can be cancelled; there is no request body.
Endpoint#
curl -X POST https://uat.auxcore.net/api/v1/invoices/0b8e6a3c-5d2f-4c71-9e1a-7f3b2c9d4e10/cancel \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "X-Tenant-ID: your-tenant-id"Response: the invoice with "status": "cancelled", cancelledAt and cancelledBy. The
server releases every active inventory reservation before committing the terminal status. The
void endpoint follows the same release rule.
| Rejected (400) | Message |
|---|---|
paid |
Cannot cancel paid invoices |
cancelled |
Invoice is already cancelled |
partial |
Cannot cancel an invoice that has received partial payment |
overdue |
Cannot cancel an overdue invoice |
draft / pending |
A draft invoice has not been issued and cannot be voided — delete it instead |
Delete Invoice#
Soft-delete a draft or cancelled invoice. Response: { "success": true, "data": { "message": "Invoice deleted successfully" } }.
Any other status is refused, but the API currently reports it as 500 INTERNAL_ERROR ("Failed to delete invoice") rather than a 400; 404 NOT_FOUND when the invoice does not exist.
Endpoint#
curl -X DELETE https://uat.auxcore.net/api/v1/invoices/0b8e6a3c-5d2f-4c71-9e1a-7f3b2c9d4e10 \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "X-Tenant-ID: your-tenant-id"Generate QR Code#
Get a QR code for the invoice's pay link.
Endpoint#
curl https://uat.auxcore.net/api/v1/invoices/0b8e6a3c-5d2f-4c71-9e1a-7f3b2c9d4e10/qr-code \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "X-Tenant-ID: your-tenant-id"Response:
{
"success": true,
"data": {
"qrCode": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..."
}
}Partial Payment Flow#
AuxCORE fully supports partial payments:
- Enable partial payments with
allowPartialPayment: true(optionally with a minimum payment rule) - Customer pays any amount between the minimum and the remaining balance
- Status updates automatically:
sent/viewed→partial(after first payment)partial→paid(when fully paid)
- Every payment is recorded in
paymentHistory
Inventory does not re-check availability to approve a payment. Stock that changed after Send does
not block the payment. At the first fully-paid transition, any quantity that cannot be deducted is
recorded as notDeductedQuantity and surfaced through inventory Pending Review; the Invoice stays
paid.
Migration and Legacy Open Invoices#
The location/reservation migration is additive. invoiceLocationType is nullable and existing
Invoices are not backfilled from metadata.location or any default. Existing opensent/viewed/partial/overdue Invoices are not silently given reservations during rollout.
They remain payable. A new reservation starts only when an explicit post-rollout Send or eligible
pre-payment edit supplies and validates the fulfillment contract. If a legacy open Invoice reaches
full payment without an owned reservation, the server follows the compatibility settlement path;
stock shortfall or a missing/inactive fulfillment source becomes a durable not-deducted/Pending
Review outcome and never reverses the payment.
Example Flow#
# Create invoice with partial payments enabled
POST /api/v1/invoices
{"merchantId": "6f1c2a9e-...", "amount": 1000.00, "allowPartialPayment": true}
# Status: sent, amountDue: 1000.00
# Customer pays $300 (recorded after the charge)
POST /api/v1/invoices/0b8e6a3c-.../payment
{"amount": 300.00, "paymentMethod": "card"}
# Status: partial, amountPaid: 300.00, amountDue: 700.00
# Customer pays $700
POST /api/v1/invoices/0b8e6a3c-.../payment
{"amount": 700.00, "paymentMethod": "card"}
# Status: paid, amountPaid: 1000.00, amountDue: 0.00Webhooks#
When an invoice has a webhookUrl, AuxCORE POSTs one event to it after a successful customer card or ACH payment through the pay link. No other invoice events are emitted.
invoice.paid#
{
"event": "invoice.paid",
"invoice": { "id": "0b8e6a3c-...", "invoiceNumber": "INV-2026-000123", "status": "paid", "amountDue": 0 },
"transaction": { "transactionId": "TXN-20261002-9C1D4E", "amount": 1889.10 }
}The event fires for partial payments too — check invoice.status and invoice.amountDue. The request is not signed and is not retried.
Best Practices#
✅ DO:#
- Use clear descriptions in line items
- Set reasonable due dates (15-30 days)
- Enable partial payments for flexibility
- Send reminders for overdue invoices
- Let the server calculate totals - send quantities, prices, discounts and tax rates
❌ DON'T:#
- Don't send without reviewing - save as
draftfirst - Don't use vague descriptions - be specific
- Don't skip due dates
- Don't try to
PUT-update a sent invoice - use Edit Invoice instead; it rotates the payment link and re-sends the updated invoice so the customer always pays through the latest link
Next Steps#
Need help? Contact support@auxcore.net