Developer dashboard

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:

http
Authorization: Bearer YOUR_TOKEN
X-Tenant-ID: your-tenant-id
Content-Type: application/json

X-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, or
null 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 writable
lineItems[].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 with
409 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:

json
{
  "metadata": { "location": "Headquarters" }
}

After this contract, invoiceLocationType is added while the top-level stockLocationId continues
to carry the Invoice-level physical fulfillment ID:

json
{
  "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:

json
{
  "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):

json
{
  "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#

POST/api/v1/invoices/inventory-preflight

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 returns
403 FORBIDDEN.

Request#

Requestjson
{
  "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 and
locationName, 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#

POST/api/v1/invoices

Request#

Requestbash
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's discount lowers that line's taxable base, but the invoice total only subtracts the invoice-level discountAmount. Send discountAmount as 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:

Responsejson
{
  "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#

POST/api/v1/invoices/:invoiceId/send
Send an Invoicebash
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:

Send an Invoicejson
{
  "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#

POST/api/v1/invoices/:invoiceId/payment
Record a Paymentbash
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#

GET/api/v1/invoices/:invoiceId
Get Invoicebash
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:

  • fulfillmentLocationLocked and fulfillmentLocationLockReason (first_payment or null)
  • inventoryReservation.state: none, active, partial, converted_to_sale, or released
  • inventoryReservation.lines[]: Product, physical location, requested/reserved/unreserved
    quantities, persisted line state, and updatedAt
  • inventoryActivity[]: server-recorded reservation and settlement quantities; clients must not
    infer Activity from balance totals
  • Historical rows always include locationId. locationName is 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.
Get Invoicejson
{
  "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#

GET/api/v1/invoices/lookup
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#

GET/api/v1/invoices

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
Query Parametersbash
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:

Query Parametersjson
{
  "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#

GET/api/v1/invoices/summary

Optional query: merchantId.

Invoice Summaryjson
{
  "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#

PUT/api/v1/invoices/:invoiceId
Update Invoicebash
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 old linkId is invalidated (it now serves a "link no longer available" message) and a new linkId / shortLink is issued. Status is set to sent and 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 and allowRescheduleNextPayment only. 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 with 400.
  • paid → 409 CONFLICT. cancelled / expired / overdue / pending → 400 VALIDATION_ERROR.

Endpoint#

POST/api/v1/invoices/:invoiceId/edit

Full edit (draft / sent / viewed)#

Full edit (draft / sent / viewed)bash
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 returns
409 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).

Full edit (draft / sent / viewed)json
{
  "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" } }
}

shortLink may 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.delivery reports failed); call send-email / send-sms to retry delivery.

Partial edit (due date + notes only)#

Partial edit (due date + notes only)bash
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.

Once a full edit rotates the link, the old pay URL (/pay/invoice/{oldLinkId}) resolves to:

Old payment link after a full editjson
{
  "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#

POST/api/v1/invoices/:invoiceId/cancel
Cancel Invoicebash
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#

DELETE/api/v1/invoices/:invoiceId
Delete Invoicebash
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#

GET/api/v1/invoices/:invoiceId/qr-code
Generate QR Codebash
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:

Generate QR Codejson
{
  "success": true,
  "data": {
    "qrCode": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..."
  }
}

Download PDF#

Get invoice as PDF (Content-Type: application/pdf, sent as an attachment).

Endpoint#

GET/api/v1/invoices/:invoiceId/pdf
Download PDFbash
curl https://uat.auxcore.net/api/v1/invoices/0b8e6a3c-5d2f-4c71-9e1a-7f3b2c9d4e10/pdf \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id" \
  -o invoice.pdf

Partial Payment Flow#

AuxCORE fully supports partial payments:

  1. Enable partial payments with allowPartialPayment: true (optionally with a minimum payment rule)
  2. Customer pays any amount between the minimum and the remaining balance
  3. Status updates automatically:
    • sent / viewed → partial (after first payment)
    • partial → paid (when fully paid)
  4. 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 open
sent/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#

bash
# 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.00

Webhooks#

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#

json
{
  "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 draft first
  • 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