DOCUMENTATION

API Documentation

API Documentation

Authentication

Sign in to your Chargetree account and go to Settings → API. Click Generate API key. The plaintext key is shown exactly once. Save it in your secret store immediately.

Rotating a key immediately invalidates the previous one. There is no grace period. Plan rotations during a maintenance window.

Generate an API Key

Sign in to your Chargetree account and go to Settings → API. Click Generate API key. The plaintext key is shown exactly once. Save it in your secret store immediately. Keys have the format:

ct_live_<32 characters> # eg. ct_live_8x3kQp9zR2mN6vT4yL1bH7sW0jD5fA3c

The key belongs to the account, not the user – team members leaving the account do not invalidate the key.
Rotating a key immediately invalidates the previous one. There is no grace period. Plan rotations during a maintenance window.

Bearer token

Every request must include:

Authorization: Bearer ct_live_<your key>

Error codes

Header

Missing

Not a Bearer scheme

Valid Bearer, unknown or revoked key

Response

401 UNAUTHENTICATED

401 UNAUTHENTICATED

401 INVALID_API_KEY

  • HTTPS only. Plain-HTTP requests are rejected before they reach the handler.

  • No CORS. Server-to-server only. Browser-based integrations must proxy through your own backend.

  • Keys are stored hashed. Only the SHA-256 digest lives in our database. If you lose the plaintext, regenerate.

Rate limits

Quotas apply per API key, per rolling minute:

Method

GET

POST / PUT / DELETE

Limit / minute

120

30

Limit / hour

5,000

1,000

When you exceed a quota, the response is:

HTTP/1.1 429 Too Many Requests

Retry-After: 42

X-RateLimit-Limit: 30

X-RateLimit-Remaining: 0

X-RateLimit-Reset: 1735689600

{

"error": {

"code": "RATE_LIMITED",

"message": "API rate limit exceeded. Try again after the Retry-After window."

}

}

Recommended client behaviour: back off for Retry-After seconds, then retry. Don't burn your quota with tight-loop retries — you'll just get more 429s.

Errors

All errors return the same envelope:

Method

GET

POST / PUT / DELETE

Limit / minute

120

30

Limit / hour

5,000

1,000

When you exceed a quota, the response is:

{

"error": {

"code": "VALIDATION_ERROR",

"message": "Human-readable message.",

"field_errors": [

{ "field": "line_items[0].unit_amount", "message": "must be >= 0" }

]

}

}

Code

UNAUTHENTICATED

INVALID_API_KEY

RATE_LIMITED

VALIDATION_ERROR

NOT_FOUND

INVOICE_LOCKED

INVOICE_NOT_CANCELLABLE

DUPLICATE_INVOICE_NUMBER

PLAN_LIMIT_REACHED

ACCOUNT_NOT_READY_TO_SEND

INTERNAL_ERROR

HTTP

401

401

429

400

404

403

400

409

402

409

500

Description

Missing or malformed Authorization header

Key not found or revoked

Over the per-key quota

Bad request shape; see `field_errors`

Resource does not exist in this account

Financial fields on a paid invoice cannot change

Attempted to cancel a PAID invoice

invoice_number already used in this account

Subscription blocks new invoices. Upgrade your plan

send_invoice: true but account has no checkout slug

Something broke on our side

Invoices

Create an invoice

POST /api/v1/invoices

Creates an invoice. If the referenced contact does not exist, one is created (or matched by `external_id`/`email` within your account).

{

"contact": {

"id": "existing-contact-uuid",

"name": "Acme Pty Ltd",

"email": "ap@acme.example",

"phone": "+61 400 000 000",

"external_id": "crm-12345",

"address": {

"line1": "1 George St",

"line2": "",

"city": "Sydney",

"region": "NSW",

"postal_code": "2000",

"country": "AU"

}

},

"invoice_number": "INV-1042",

"invoice_date": "2026-07-01",

"due_date": "2026-07-31",

"reference": "PO-9981",

"notes": "Thanks for your business.",

"staff": "Cory Mayfield",

"currency_code": "AUD",

"line_amount_types": "Exclusive",

"line_items": [

{

"description": "Onsite EV charger installation",

"quantity": 1,

"unit_amount": 1850.00,

"tax_type": "OUTPUT2",

"tax_rate": 10,

"account_code": "200",

"sort_order": 0

}

],

"send_invoice": true

}

Request fields

Request fields

Request fields

Field

contact.id

contact.name

contact.email

contact.phone

contact.external_id

invoice_number

invoice_date

due_date

reference

notes

currency_code

line_amount_types

line_items

line_items[].description

line_items[].quantity

line_items[].unit_amount

line_items[].tax_type

line_items[].tax_rate

line_items[].account_code

line_items[].sort_order

send_invoice

Required

No

Yes

Yes

Yes

No

No

No

No

No

No

No

No

No

Yes

Yes

Yes

No

No

No

No

No

Type

string

string

string

string

string

string

date

date

string

string

string

enum

string

string

number

number

string

number

string

number

boolean

Description

UUID of an existing contact in your account.

Name of contact. Required if contact.id is not used.

Email address for the contact. Required if contact.id is not used.

Mobile number for the contact. Include the international code.

Your system's identifier for the contact.

Invoice number (must be unique). Auto-generated if not sent in the request.

ISO-8601 date (YYYY-MM-DD). Defaults to today if empty.

ISO-8601 date (YYYY-MM-DD). Defaults to today if empty.

Free-form reference (PO number, etc.). Shown to the customer.

Free-form notes appended to the invoice.

ISO 4217 (e.g. AUD). Defaults to your account currency.

Exclusive (default), Inclusive, or NoTax.

At least one line item required.

Description of the line item.

Must be > 0.

Must be >= 0

Xero tax types: OUTPUT (GST), EXEMPTOUTPUT, BASEXCLUDED.

Percentage 0-100. Defaults to 10.

Xero account code for revenue attribution.

Display order on the invoice.

If true, emails the invoice to the customer immediately after creation

Retrieve an invoice

POST /api/v1/invoices/:id

Returns the invoice or 404 NOT_FOUND if the id doesn't belong to your account.

List invoices

GET /api/v1/invoices

Cursor-paginated. Sort order is always created_at DESC.

Query parameters

Query parameters

Query parameters

Parameters

status

contact_id

invoice_number

date_from

date_to

limit

cursor

Description

Choose from DRAFT, UNPAID, OVERDUE, PARTIALLY_PAID, PAID, CANCELLED, SCHEDULED

Filter by contact UUID.

Exact match.

ISO date. Returns invoices with invoice_date >= date_from

ISO date. Returns invoices with invoice_date <= date_to

Page size. Default 25, max 100

Opaque cursor from the previous response's next_cursor

Response

Response

Response

{
"data": [ /* invoice objects */ ],
"next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wNy0wMVQwMDowMDowMFoiLCJpZCI6IjEyMyJ9",
"has_more": true
}

When has_more is false, next_cursor is null.

Example: paginate through all invoices

Example: paginate through all invoices

Example: paginate through all invoices

async function* allInvoices() {
let cursor = null;
while (true) {
const url = new URL('https://app.chargetree.co/api/v1/invoices');
url.searchParams.set('limit', '100');
if (cursor) url.searchParams.set('cursor', cursor);

const res = await fetch(url, { headers: { Authorization: `Bearer ${API_KEY}` }, }); const { data, next_cursor, has_more } = await res.json(); for (const invoice of data) yield invoice; if (!has_more) return; cursor = next_cursor;

}
}

Update an invoice

PUT /api/v1/invoices/:id

Partial update — supply only the fields you want to change.

Editable fields

Editable fields

Editable fields

Field

contact.id

reference

notes

staff

send_invoice

invoice_number

invoice_date

due_date

currency_code

line_amount_types

line_items (full replace)

Editable when unlocked

Editable when locked

An invoice is locked once a payment has been recorded against it. Locked invoices reject financial-field changes with 403 INVOICE_LOCKED.

Line item replacement

Line item replacement

Line item replacement

When you include line_items in the payload, they fully replace the existing items. Partial line-item updates are not supported. Totals are recalculated.

Request

Request

Request

{

"reference": "PO-9981-v2",

"notes": "Updated to reflect scope change.",

"line_items": [

{ "description": "Onsite install", "quantity": 1, "unit_amount": 2000, "tax_rate": 10, "tax_type": "OUTPUT" },

{ "description": "Extended warranty", "quantity": 1, "unit_amount": 250, "tax_rate": 10, "tax_type": "OUTPUT" }

]

}

Cancel an invoice

POST /api/v1/invoices/:id/cancel

Soft-cancels the invoice: sets status: "CANCELLED", amount_due: 0. If the invoice was previously synced to Xero, we best-effort void it there too — the local cancellation succeeds either way.

Request

Request

Request

{

"reason": "Customer requested cancellation"

}

reason is optional, must be a string, and is stored on the audit log entry.

Restrictions

Restrictions

Restrictions

  • Cannot cancel a PAID invoice – returns 400 INVOICE_NOT_CANCELLABLE.

  • Cancelling twice is safe; the second call just re-writes the same state.

Invoice object

{

"id": "717f2bfc-c6d4-41fd-b238-3f2f0c0cf777",

"invoice_number": "INV-1042",

"status": "UNPAID",

"contact": {

"id": "c1",

"name": "Acme Pty Ltd",

"email": "ap@acme.example",

"phone": "+61 400 000 000"

},

"invoice_date": "2026-07-01",

"due_date": "2026-07-31",

"currency": "AUD",

"line_amount_types": "Exclusive",

"sub_total": 1850.00,

"tax_total": 185.00,

"total": 2035.00,

"amount_paid": 500.00,

"amount_due": 1535.00,

"line_items": [

{

"id": "li-1",

"description": "Onsite EV charger installation",

"quantity": 1,

"unit_amount": 1850.00,

"tax_rate": 10,

"tax_amount": 185.00,

"line_amount": 1850.00

}

],

"payments": [

{

"id": "pay-1",

"amount": 500.00,

"currency": "AUD",

"received_at": "2026-07-05T09:12:00Z",

"method": "stripe",

"reference": "pi_3Nz_test",

"recorded_by": "stripe"

}

],

"reference": "PO-9981",

"notes": "Thanks for your business.",

"staff": "Cory Mayfield",

"is_editable": false,

"sent_to_contact": true,

"last_sent_at": "2026-07-01T05:12:33Z",

"send_count": 1,

"pay_url": "https://pay.chargetree.co/acme/INV-1042",

"xero_invoice_id": "abc-123",

"created_at": "2026-07-01T05:12:30Z",

"updated_at": "2026-07-05T09:12:00Z"

}

  • statusvalues: DRAFT, SCHEDULED, UNPAID, OVERDUE, PARTIALLY_PAID, PAID, CANCELLED.

  • pay_url is null when your account has no public checkout slug configured.

  • payments is chronological (oldest first).

  • payments[].method values: stripe, cash, card, bank_transfer, cheque, manual, other.

  • payments[].recorded_by values: stripe (Stripe webhook), user (recorded via the in-app UI), api (recorded via this public API).

  • is_editable flips to false once any payment is recorded.

Contacts

Create/Match an invoice

POST /api/v1/contacts

if a contact matching external_id or email already exists in your account, it's returned unchanged with matched: true. Otherwise a new one is created.

Request

Request

Request

{

"name": "Acme Pty Ltd",

"email": "ap@acme.example",

"phone": "+61 400 000 000",

"external_id": "crm-12345",

"address": {

"line1": "1 George St",

"city": "Sydney",

"region": "NSW",

"postal_code": "2000",

"country": "AU"

}

}

At least name and one of email / phone are required to create a new contact.

Matching precedence: external_id → email (case-insensitive). Matches are always scoped to your account – a matching email in a different Chargetree account is never returned.

Retrieve a contact

GET /api/v1/contacts/:id

Returns the contact or 404 NOT_FOUND.

Search a contact

GET /api/v1/contacts?email=ap@acme.example

GET /api/v1/contacts?external_id=crm-12345

Exactly one of `email` or `external_id` is required. Returns the contact or 404 NOT_FOUND.

Contact object

{

"id": "c1",

"name": "Acme Pty Ltd",

"email": "ap@acme.example",

"phone": "+61 400 000 000",

"external_id": "crm-12345",

"address": {

"line1": "1 George St",

"line2": null,

"city": "Sydney",

"region": "NSW",

"postal_code": "2000",

"country": "AU"

},

"created_at": "2026-06-01T10:00:00Z",

"updated_at": "2026-06-01T10:00:00Z"

}

The address object is `null` when no address fields are set. Only the address fields you provide on create are stored; omitted fields stay null.

Webhooks

Chargetree can POST signed event envelopes to your endpoint when payments are recorded against invoices. This lets you stay in sync without polling.

Registering an endpoint

In the Chargetree dashboard, go to Settings / API and add a webhook endpoint:
1. Enter your HTTPS URL (e.g. https://your-app.example/webhooks/chargetree).
2. Select the events you want to receive (currently only `invoice.payment_recorded`).
3. Copy the signing secret shown once at creation. Store it in your secret manager.


Up to 5 active endpoints per account.

Supported events

Supported events

Supported events

Event type

invoice.payment_recorded

Trigger

A payment is recorded against an invoice.

Event envelope

Event envelope

Event envelope

Chargetree POSTs the envelope to your URL as JSON.

POST https://your-app.example/webhooks/chargetree HTTP/1.1

Content-Type: application/json

Chargetree-Signature: 3l2Kbf4x7HvR9GqJm2pN0aBcDeFgHiJkLmNoPqRsTuVw

User-Agent: Chargetree-Webhooks/1.0

{

"events": [

{

"resource_url": "https://app.chargetree.co/api/v1/invoices/717f2bfc-c6d4-41fd-b238-3f2f0c0cf777",

"resource_id": "717f2bfc-c6d4-41fd-b238-3f2f0c0cf777",

"event_date_utc": "2026-07-15T03:14:22.451",

"event_type": "Payment",

"event_category": "INVOICE",

"account": "c2cc9b6e-9458-4c7d-93cc-f02b81b0594f"

}

]

}

Envelope fields

Envelope fields

Envelope fields

Field

events[].resource_url

events[].resource_id

events[].event_date_utc

events[].event_type

events[].event_category

events[].account

Description

GET this URL (with your API key) to fetch the canonical current state of the invoice.

The Chargetree invoice UUID. Same as the id in resource_url.

When the payment was recorded, server-side, with millisecond precision.

Always Payment for invoice.payment_recorded

Always INVOICE for this event.

The Chargetree account UUID.

Why the payload doesn't embed the invoice

Why the payload doesn't embed the invoice

Why the payload doesn't embed the invoice

The envelope points at `resource_url` rather than including the invoice inline. Reason: if delivery is delayed (e.g. hit a retry), you always see the latest state when you fetch via GET rather than a stale snapshot from when the event was queued.

Verifying signatures

Every request includes a Chargetree-Signature header — base64-encoded HMAC-SHA256 of the raw request body, using your endpoint's signing secret as the key.

import { createHmac, timingSafeEqual } from 'crypto';

import express from 'express';

const SIGNING_SECRET = process.env.CHARGETREE_WEBHOOK_SECRET!;

function verifyChargetreeSignature(

rawBody: string,

signatureHeader: string | null,

signingSecret: string,

): boolean {

if (!signatureHeader) return false;

const expected = createHmac('sha256', signingSecret).update(rawBody).digest();

let received: Buffer;

try {

received = Buffer.from(signatureHeader, 'base64');

} catch {

return false;

}

if (received.length !== expected.length) return false;

return timingSafeEqual(received, expected);

}

const app = express();

// IMPORTANT: use express.raw so req.body is a Buffer of the raw bytes.

// express.json() parses the body BEFORE you can verify it, which breaks the HMAC.

app.post(

'/webhooks/chargetree',

express.raw({ type: 'application/json' }),

(req, res) => {

const raw = req.body.toString('utf8');

const sig = req.get('Chargetree-Signature');

if (!verifyChargetreeSignature(raw, sig, SIGNING_SECRET)) {

return res.status(401).send('bad signature');

}

const { events } = JSON.parse(raw);

for (const event of events) {

// Process each event...

}

res.status(200).send('ok');

},

);

Delivery guarantees

  • Timeout: your endpoint must respond within 15 seconds.

  • Retry schedule on non-2xx or timeout: 1 minute → 5 minutes → 30 minutes → 2 hours → 12 hours — 5 attempts total.

  • Retries are best-effort — the actual retry time depends on when the retry worker's cron tick runs.

  • After 5 failed attempts the delivery is marked exhausted. It won't be retried further.

  • Successful deliveries are not retried.

  • The dashboard's Settings → API page shows recent delivery attempts per endpoint for debugging.

Handler best practices

  • Always verify the signature. Reject unverified requests with 401.

  • Return 200 OK quickly. Do the actual processing asynchronously (job queue, worker, etc.) — the 15-second timeout is a hard limit.

  • Fetch the current state via resource_url. The envelope intentionally doesn't embed the invoice. GET the URL to see the latest values. This works whether the event fires now or 12 hours later after retries.

  • Don't rely on ordering. If two payments hit within seconds, delivery order isn't guaranteed. Sort by event_date_utc if ordering matters.

  • Log the raw body. Store the full envelope + signature for at least 30 days. Makes debugging signature-verification failures trivial.

Support

Questions or issues? Email support@chargetree.co.

Collections that feels human and actually works

Everything you need to recover overdue invoices at scale, without chasing or awkward conversations.

Start a free trial

Automated collections, built for real relationships.

LEGAL

Privacy

© 2026 Chargetree Pty Ltd. All rights reserved

Collections that feels human and actually works

Everything you need to recover overdue invoices at scale, without chasing or awkward conversations.

Start a free trial

Automated collections, built for real relationships.

LEGAL

Privacy

© 2026 Chargetree Pty Ltd. All rights reserved

Collections that feels human and actually works

Everything you need to recover overdue invoices at scale, without chasing or awkward conversations.

Start a free trial

Automated collections, built for real relationships.

LEGAL

Privacy

© 2026 Chargetree Pty Ltd. All rights reserved