00Read this first
Partner checkout builds on the platform-wide API admin and OAuth flows. It is written for external developers who cannot inspect the source code. Read these guides before implementing checkout:
Those guides explain how to obtain access tokens, configure scopes, and authenticate as an API admin reseller.
01What partner checkout does
Partner checkout lets an eligible reseller create and manage a billed-account signup on behalf of a new client. The API can:
- discover which products and add-ons are currently valid for the reseller
- list allowed billing countries and zone codes
- check whether an email, ABN, or NZ employer IRD number is already in use
- preview pricing and validation results before creating anything
- create the customer, address, company record, order, subscription, add-ons, totals, and post-pay record
- optionally send the client a welcome email with a password-reset link
- replay a previously successful execute request safely by idempotency key
- list, inspect, and cancel orders created by that reseller
- end a client's paid subscription early, after previewing exactly what would end
- start a client on a free trial with no order and nothing to pay, then convert that trial into a paid subscription later (see "Free Trials")
02Base URL and authentication
- Base path:
/api - Auth header:
Authorization: Bearer <access_token> - Caller must be an API admin reseller in customer group
9
If the authenticated customer is not an API admin, or is not in group 9, the API returns 403.
03OAuth scopes
| Purpose | Scope |
|---|---|
| Product discovery | partner.checkout.preview or partner.checkout.write or partner.checkout.cancel |
| Availability checks | partner.checkout.preview or partner.checkout.write or partner.checkout.cancel |
| Zone discovery | partner.checkout.preview or partner.checkout.write or partner.checkout.cancel |
Preview order (dry_run=true) | partner.checkout.preview |
Execute order (dry_run=false) | partner.checkout.write |
Preview an upgrade (dry_run=true) | partner.checkout.preview |
Execute an upgrade (dry_run=false) | partner.checkout.write |
| Cancel order | partner.checkout.cancel or partner.checkout.write |
| List or inspect past orders | Any partner checkout scope above |
| Create a free trial | partner.checkout.write |
| List trials | Any partner checkout scope above |
| Cancel a trial | partner.checkout.cancel or partner.checkout.write |
| List client renewals | Any partner checkout scope above |
| Change a client's renewal | partner.checkout.write |
| End a client's subscription early | partner.checkout.cancel or partner.checkout.write; a dry run also accepts partner.checkout.preview |
| Look up a client | Any partner checkout scope above |
openid is still required on the OAuth authorization request. Legacy openapi remains accepted in the wider OAuth flow.
04Recommended integration sequence
GET/api/partner-checkout/options
GET/api/partner-checkout/zones
Optional: GET/api/partner-checkout/availability?email=...&abn=...
or ...&irdNumber=...
POST/api/partner-checkout/orders with dry_run=true
Same call again with the same business data, dry_run=false, and an Idempotency-Key
Persist the returned customer_id, order_id, and subscription_id
Use GET/api/partner-checkout/orders and GET/api/partner-checkout/orders/{order_id} for reconciliation and support
Use POST/api/partner-checkout/orders/cancel only to reverse a qualifying order within 60 days of placing it. Send end_subscription: true when the client should also lose access; a cancel on its own leaves access running until the expiry date. Once the order's subscription has renewed, end the renewed one with /subscriptions/end instead
To end a client's paid subscription early, call POST/api/partner-checkout/subscriptions/end with dry_run=true first, then dry_run=false. Nothing is refunded or credited, so use /upgrades instead to move a client to another plan
To let the client evaluate the product before paying, start at "Free Trials" below instead of step 4, then come back to steps 4 and 5 with end_customer_id when the trial ends.
Do not hardcode product IDs, add-on IDs, pricing, or zone codes. Discover them dynamically.
05Country, identifier & address rules
Only Australia (AU) and New Zealand (NZ) are supported. Country is inferred from the company identifier:
Australia
- Send
company.abn
New Zealand
- Send
company.ird_number
- You must provide exactly one of
abnorird_number. billing_address.country_codeis not accepted in requests.billing_address.companyis not accepted in requests.customer.passwordis not accepted in requests.billing_address.zone_codemust match the inferred country.zone_codeis case-insensitive in the request and normalized to uppercase.- If a zone is invalid, the API returns
422and includes the valid codes for that country in the error detail.
06Endpoint reference
1. Discover products and pricing
Use this endpoint to discover the current subscription products, add-on products, pricing, required scopes, and reseller-specific mandatory requirements.
Important response areas:
subscription_products: valid base products fororder.product_idadd_on_products: optional or required add-onsmandatory_requirements.training_session_product_id: the optional training session product you can add viaorder.include_training_sessionmandatory_requirements.training_session_product_name/mandatory_requirements.training_session_prices: display name and per-country (AU/NZ) retail pricing for the optional training session line, so the setup fee can be shown before previewing an order. These match the one-offtraining_sessionline returned by preview/execute when requested (full retail price, no partner discount, RRP equal to its own price)mandatory_requirements.required_owned_add_on_keys: branded add-on families this reseller must include for new client signupsper_employee_minimum: minimum quantity for per-employee base productsshow_per_employee_pricing: whether this reseller can use per-employee plansprices: country-specific partner pricing and, where available, retail RRP comparison values
2. Discover zones
Use this endpoint to fetch the supported countries and valid zone_code values for billing addresses. The response currently includes AU and NZ. Use the returned zone.code values as billing_address.zone_code.
3. Check availability
Use this endpoint to check whether one or more proposed identifiers are already in use.
Query parameters: email, abn, irdNumber. You can send any combination of these in one request, but at least one must be provided.
Each result includes provided, normalized, valid_format, exists, available, message, required_owned_add_ons, has_all_required_owned_add_ons.
Normalization behaviour: emails are trimmed and lowercased; ABNs and IRD numbers are digit-normalized before validation.
The response also always includes a coarse claim object. Only a valid email plus exactly one valid country identifier can return eligible or ineligible; partial or invalid checks return incomplete. Stable status/message pairs are not_applicable/no_existing_customer, incomplete/claim_check_incomplete, eligible/existing_trial_claimable, and ineligible/existing_customer_not_claimable. This check is guidance only: it takes no lock or reservation, and never returns a matched customer ID.
4. Preview or execute an order
Use this endpoint to preview validation and pricing with dry_run=true, or to create the full customer and order with dry_run=false.
Headers: Authorization: Bearer <token>, Content-Type: application/json, Idempotency-Key: <unique value> required for dry_run=false.
Request body
| Field path | Type | Required | Notes |
|---|---|---|---|
dry_run | boolean | Yes | true validates and prices only. false creates records. |
claim_existing_trial | boolean | No | When omitted and end_customer_id is absent, checkout automatically attempts the strict safe claim for an eligible, commercially untouched self-service trial. true explicitly requests the same behaviour; false opts out and preserves the duplicate-email 409. true cannot be combined with end_customer_id. See "Claiming an existing self-service trial". |
end_customer_id | integer | No | Order for a client you already provisioned instead of creating a new one. Use this to convert a free trial. See "Ordering for an existing client". |
send_customer_welcome_email | boolean | No | Default true. Sends a branded onboarding email with a reset link for a new customer. Ignored for an existing-customer conversion/claim and when oauth_onboarding is supplied. |
customer.first_name | string | Yes | 1-32 chars |
customer.last_name | string | Yes | 1-32 chars |
customer.email | Yes | Must be unique unless an authorised end_customer_id conversion or the automatic safe-claim path reuses an eligible matching customer. Send claim_existing_trial=false to retain the duplicate-email 409. | |
customer.phone | string | Yes | 3-32 chars |
company.legal_name | string | Yes | 1-128 chars |
company.abn | string | Conditional | AU only. Exactly one of abn or ird_number must be sent. |
company.ird_number | string | Conditional | NZ only. Exactly one of abn or ird_number must be sent. |
billing_address.address_1 | string | Yes | 3-128 chars |
billing_address.address_2 | string | No | Max 128 chars |
billing_address.city | string | Yes | 2-128 chars |
billing_address.postcode | string | Yes | 2-10 chars |
billing_address.zone_code | string | Yes | Must be a valid zone for the inferred country |
order.product_id | integer | Yes | Must be a subscription product returned by /options |
order.add_on_product_ids | integer[] | No | Optional compatible add-ons; max 50 IDs |
order.extra_company_qty | integer | No | Additional company slots where supported |
order.per_employee_qty | integer | Conditional | Required for per-employee products; must meet reseller minimum |
order.add_free_trial_month | boolean | No | Default true. Adds one extra free month, so a monthly signup gets 2 months and an annual signup gets 13. Granted once per client: if they already had a free trial, it is not granted again. See "The free month is once per client". |
order.subscription_end_date | string (ISO date) | No | Optional aligned subscription expiry. Triggers prorated pricing on the base subscription and recurring add-ons. See "Subscription term alignment & prorating". |
order.include_training_session | boolean | No | Default false. When true, adds a one-off Training & Setup Session line (product 260) at full retail price - no reseller discount, no proration. Omit or leave false to skip it entirely; no training-session line is added and product 260 does not need to be configured. |
order.client_reference | string | No | Reseller's own reference, max 255 chars |
order.metadata | object | No | Up to 50 keys and under 4 KB serialized |
oauth_onboarding | object | No | Opt in to single-flow OAuth onboarding. See "OAuth single-flow onboarding" below. |
oauth_onboarding.redirect_uri | string | Conditional | Required when oauth_onboarding is sent. Must exactly match a redirect URI registered on your OAuth client. |
oauth_onboarding.scope | string | No | Default openid payroll.write. Must include openid (legacy openapi accepted) and payroll.write. |
oauth_onboarding.state | string | No | Opaque value echoed back to your redirect_uri. Auto-generated if omitted. |
Business rules
- Duplicate customer email, ABN, and IRD values block checkout, unless they belong to the client named by
end_customer_idor checkout safely claims an eligible self-service trial. Sendclaim_existing_trial=falseto opt out of that automatic claim. - Unknown fields are rejected with
422. - Base
product_idmust be a subscription product, not an add-on. - Per-employee products are only allowed when the reseller is configured for them.
- Non-per-employee products reject
per_employee_qty. - Per-employee products reject
extra_company_qty. extra_company_qtymay be capped to the plan limit; when capped, a warning is returned.- Extra-company add-ons must not be passed directly in
add_on_product_ids; useextra_company_qty. - Every chosen add-on must match the base product's billing cycle.
- If the reseller owns required branded add-on integrations, the corresponding partner add-on products must be included.
- The training session product is optional. Set
order.include_training_session: trueto add it; omit it or leave it false to skip it. When requested, the product must be configured and active or the request 400s. - When
subscription_end_dateis supplied, the base subscription and recurring add-ons are prorated; a requested training session line is still charged at full price. See "Subscription term alignment & prorating".
Preview response
When dry_run=true, nothing is created. The response still contains the fully resolved pricing and address information:
mode = "preview"validated = truecustomer_id,address_id,order_id, andsubscription_idarenullorder_linesshows the base subscription, the training session line when requested viainclude_training_session, and any add-onstotalsshows subtotal, tax, total, and any welcome-offer promo discountwarningsdescribes non-fatal conditions such as capped extra-company quantity or the free trial monthfree_trial_month_appliedsays whether a free month was granted on this orderclaimed_existing_trialremainsfalsein preview because no claim has committed
Execute response
When dry_run=false, the API creates the normal billed-account order, totals, products, subscription, post_pay, history, and optional add-ons/promo. It creates a customer and company for a new signup, or safely reuses the existing customer/company for an authorised conversion or claim.
Important execute response fields: customer_id, address_id, order_id, subscription_id, order_status_id, claimed_existing_trial, emails_sent, idempotency_replayed.
The magic onboarding link (when oauth_onboarding is used) is never returned in the response. It is emailed only to the customer's verified address.
OAuth single-flow onboarding (oauth_onboarding)
Normally, provisioning a customer and getting an OAuth payroll.write token for them are two separate loops: you create the order, the customer sets a password from the welcome email, then they (or you) run the OAuth ceremony separately before you can create their payroll company.
Supplying an oauth_onboarding block collapses that into a single flow:
You send dry_run=false with oauth_onboarding.redirect_uri (and optionally scope / state).
The new customer is emailed a single-use, 7-day magic link. Opening it signs them in without a password and drops them straight onto your OAuth consent screen.
With one click of consent, their browser is redirected to your redirect_uri with ?code=...&state=....
You exchange the code at POST /api/oauth/token for a payroll.write access token, then create their company with PUT /api/company/create.
No second login and no separate OAuth round-trip are required. After consent the customer is given the chance to set a password (or skip and do it later) before landing back on your callback.
Requirements and behaviour:
- You must have a registered OAuth client on your account (a 1-to-1 client owned by your api-admin customer). The
redirect_urimust exactly match one of its registered redirect URIs, andscopemust includepayroll.write. Adry_run=truerequest validates all of this without creating anything or sending email. - The magic link is emailed only to the customer's verified address and is never returned in the API response. Handing a partner a passwordless login link would bypass the consent this flow exists to capture.
- The link is single-use and expires after 7 days. Re-provisioning (a new order) issues a fresh link.
oauth_onboardingreplaces the password-reset welcome email;send_customer_welcome_emailis ignored when it is supplied.
See also the OAuth Authentication Guide for the token exchange this flow feeds into.
5. Cancel an order
Use this endpoint to cancel an order previously created by the authenticated reseller, including an upgrade order. Cancelling voids the charge (order_status_id becomes 7) and removes the add-ons sold on the order.
{
"order_id": 192484,
"reason": "Client signed up in error.",
"end_subscription": true
}
- Reseller must own the order.
- Order must not already be cancelled.
- Cancellation is only allowed within 60 days of the order's
date_added. partner.checkout.cancelis preferred, butpartner.checkout.writeis also accepted.- Access. On its own a cancel does not end the client's access: the order's subscription keeps working until its expiry date, and it does not renew. Send
end_subscription: trueto end that access now as well (its expiry is set to yesterday). Any subscription it replaced whose access is still running is ended with it, the same setPOST /api/partner-checkout/subscriptions/endends. Each subscription ended is listed inended_subscription_ids.end_subscriptiondefaults tofalse, so existing integrations behave as before. To end access after the 60-day window, or without voiding the charge, usePOST /api/partner-checkout/subscriptions/end. - When
end_subscription: trueis refused. The cancel returns409and changes nothing when a subscription on the order has since been renewed or upgraded into one that still runs (the client's access is on that one, which this cancel cannot end: cancel withoutend_subscriptionand end the subscription the message names with/subscriptions/end), or when a subscription it would end carries add-ons sold on another order that is still billed (cancel the order the message names first, or cancel withoutend_subscriptionand end the subscription separately). - Renewals. Either way, a pending renewal order of the order's subscriptions is cancelled, because a subscription sold on a cancelled order never renews. Each is listed in
voided_renewal_order_ids. - Add-on upgrade orders. Cancelling one removes only the add-ons it sold. The subscription they were added to is unchanged, even with
end_subscription: true. If that subscription was later upgraded with credit, which counted those add-ons' remaining value, the cancel returns409: cancel the later upgrade order first, withend_subscription: true. The check is cautious: any credit on that upgrade blocks the cancel, even when the credit did not cover these particular add-ons. - Base or both-mode upgrade orders. Cancelling one must reverse the upgrade, so it requires
end_subscription: trueand returns409without it. The upgrade's new subscription is ended, and the subscription it replaced becomes current again with its own expiry. That subscription is listed inrestored_subscription_ids, and its own order gets a history note; check itsrenewal_stateto see whether it will renew. A replaced subscription whose own expiry has already passed is made current again but grants no access, so it is not listed. - An order whose subscription was later upgraded with credit returns
409: the upgrade counted the subscription's remaining value as paid. Cancel the upgrade order first, withend_subscription: true, then this one. A subscription that was only renewed does not block a cancel withoutend_subscription. - An upgrade whose new subscription has since been renewed or upgraded again returns
409; contact support to reverse it. - No
Idempotency-Keyis needed: cancelling twice returns409.
{
"order_id": 192484,
"previous_order_status_id": 5,
"order_status_id": 7,
"order_history_id": 5512,
"comment": "Order cancelled by partner api admin (customer_id=10452). Reason: Client signed up in error. Ended subscription(s) 98321 (expiry was 2027-09-30) as of 2026-10-05, so access has ended.",
"ended_subscription_ids": [98321],
"restored_subscription_ids": [],
"voided_renewal_order_ids": []
}
{
"detail": "Order 192530 added add-ons to subscription 98321, which was upgraded into 98455 on order 192611, and that upgrade credited the add-ons' remaining value, so this order can no longer be cancelled. Cancel order 192611 with end_subscription=true first."
}
6. Inspect one order
Returns the current status and history for one order created by the reseller, including totals and currency, customer and company details, subscription ID and expiry, cancel_window_closes, cancellable, and ascending history[].
cancellable means the order is not already cancelled and is still inside its 60-day window. A cancel can still return 409 for an upgrade order, or for an order whose subscription was later upgraded with credit; see "Cancel an order" above.
7. List orders
Lists only the orders created by the authenticated reseller, newest first. Query parameters: status, limit, offset.
8. Recover onboarding or OAuth connection
Emails the safe recovery path for the customer created by one of your orders. An unestablished customer receives a fresh single-use magic onboarding link. An established customer whose OAuth connection is missing receives a normal sign-in and authorization link instead.
Request body mirrors the oauth_onboarding block of POST /api/partner-checkout/orders:
{
"oauth_onboarding": {
"redirect_uri": "https://partner.example.com/oauth/callback",
"scope": "openid payroll.write",
"state": "partner-corr-abc123"
}
}
The original redirect_uri/scope/state are never stored, so you must supply them again. They are re-validated against your current OAuth client, so a deleted client or a changed redirect URI fails 400 rather than minting a dead link.
Rules:
- Requires
partner.checkout.write. - Reseller must own the order.
- The customer must have been provisioned with
oauth_onboardingoriginally by your account. - A customer who opened the link but never finished the consent flow can be sent a fresh one. The link is single-use, so it is spent by that first click even though onboarding never completed.
- An established customer is never sent a new passwordless link. They receive a normal OAuth authorization URL and must sign in before consenting.
- A customer who already has a live OAuth connection to your client returns
409; no email is sent. - Any previously issued, unused link is invalidated; only the newest link works.
- The magic link is emailed only to the customer's verified address and is never returned in the response.
- If the email cannot be sent, the call returns
502and no new link is issued.
{
"customer_id": 192601,
"order_id": 192484,
"email_sent": true,
"recovery_action": "magic_onboarding",
"expires_in_hours": 168,
"scope_used": "partner.checkout.write"
}
For an established but disconnected customer, recovery_action is oauth_reauthorization and expires_in_hours is null.
07Free trials
A free trial lets your client evaluate Lightning Payroll before anyone is billed. It is deliberately not an order:
- No order, order line, order total, or post-pay record is created.
- Nothing appears on an invoice, and you are not billed.
- The trial subscription is never renewed.
- The trial runs for one calendar month from the day you create it.
When the trial ends, you place the real order with POST /api/partner-checkout/orders and pass the trial's customer_id as end_customer_id.
Trial sequence
POST/api/partner-checkout/trials provisions the client and emails them their access
Persist the returned customer_id and subscription_trial_id
GET/api/partner-checkout/trials to watch days_remaining
POST/api/partner-checkout/orders with dry_run=true and end_customer_id to confirm the price
Repeat with dry_run=false and an Idempotency-Key
The free month is once per client
order.add_free_trial_month defaults to true and adds one extra month to the subscription, so a monthly signup gets 2 months and an annual signup gets 13.
That free month is granted once per client. If the client has already had a free trial, whether from POST /api/partner-checkout/trials, a signup on our website, or an earlier order of yours that carried a free month, it is not granted a second time.
When that happens the request still succeeds. It does not error:
{
"free_trial_month_applied": false,
"warnings": [
"A free trial month was already provided for this customer, so no additional free month was applied to this order."
]
}
Read free_trial_month_applied rather than assuming, so you never quote a client 13 months and deliver 12. In practice this only triggers when you send end_customer_id, because a brand-new client has no prior trial and always receives the free month.
Ordering for an existing client
Send end_customer_id on POST /api/partner-checkout/orders to order for a client you already provisioned. Then:
- No new customer record is created; the order attaches to that client.
- The
customerblock is still required, andcustomer.emailmust match that client's email, otherwise the API returns409. This stops an order being attached to the wrong account. - The
companyblock is still required. Re-sending the same ABN or IRD number is accepted because that client already holds it. - The client's billing address is updated in place from
billing_address. - Any still-running trial subscription is retired, so the paid plan's employee and company limits take effect immediately.
- A subscription still running on an order that was cancelled (or is otherwise not complete) is retired the same way: its access ends yesterday and any pending renewal order for it is cancelled. Nobody is paying for it, and leaving it running would keep its limits beside the new plan's. The dry run lists each one in
warnings, and the order's history records how many were retired. - The new subscription starts today and runs a full term.
- The password-reset welcome email is not resent, since the client already has a password. Use
POST /api/partner-checkout/orders/{order_id}/resend-onboarding-linkwhen their onboarding or OAuth connection needs recovery.
You may only pass an end_customer_id for a client you provisioned, meaning a trial you created or a client you already have an order for. Anything else returns 403. An unknown ID returns 404.
A client who still has a live paid subscription cannot be ordered for again: a second order would add a second, overlapping subscription that renews alongside the first. The dry run and the execute both return 409, and nothing is written. For a subscription you sold, the message names it and the endpoint to use instead:
{
"detail": "Customer 192601 already has an active paid subscription 88412 (expires 2027-03-31). Ordering again would add a second, overlapping subscription. To change their plan now use POST /api/partner-checkout/upgrades with subscription_id 88412; to change what they renew onto use PATCH /api/partner-checkout/renewals/88412; to stop it first use POST /api/partner-checkout/subscriptions/end."
}
- To move the client to a different plan now, with credit for the unused time, use
POST /api/partner-checkout/upgrades. - To change the plan they move onto at renewal, use
PATCH /api/partner-checkout/renewals/{subscription_id}. - To stop the subscription and then order afresh, end it with
POST /api/partner-checkout/subscriptions/end(no refund or credit), then place the order. - On the subscription's expiry day it can no longer be upgraded, and the
409says so: order again from the next day, once it has ended, or end it first. - If the live paid subscription was not sold through your account (the client bought directly, or through another reseller), the
409says so without naming it. Contact Lightning Payroll support before ordering for that client. - A subscription still running on an order you placed and later cancelled is retired by the new order (its access ends yesterday and any pending renewal order is cancelled), and the dry run lists it in
warnings. One running on an order someone else placed returns409pointing to Lightning Payroll support.
A client whose paid subscription has lapsed, was ended early, or was sold on an order that is now cancelled can be ordered for again. A retry of an order that already succeeded, with the same Idempotency-Key and payload, still replays the original response.
Claiming an existing self-service trial
If checkout finds that the email already belongs to a customer who signed up for a self-service trial, it automatically attempts to reuse that account instead of creating a duplicate or requiring a partner software change.
- Continue sending the existing new-customer order payload without
end_customer_id. Omittedclaim_existing_trialautomatically requests the safe claim. - Optionally call
GET /api/partner-checkout/availabilitywith the email and exactly one valid country identifier:abnorirdNumber, for non-authoritative guidance before preview or execute. - Preview, then execute the same payload with a new
Idempotency-Key. Sendclaim_existing_trial=trueonly when you want to state the request explicitly. - Persist the response and check
claimed_existing_trial=true. Sendclaim_existing_trial=falseonly when you deliberately require the previous duplicate-email409.
The authenticated API-admin partner and its checkout-write scope are sufficient authority for this limited commercial claim. No customer data access or OAuth consent transfers with it. claim_existing_trial=true explicitly requests the same claim; false opts out. With end_customer_id, the existing authorised conversion remains unchanged. If the email has no existing customer, the request follows normal new-customer creation and returns claimed_existing_trial=false.
The server rechecks eligibility under database locks during execute. A claim is allowed only for one normal, active self-service-trial customer with no order history, paid/renewing subscription, partner-attributed trial, conflicting live onboarding/OAuth relationship, API branding/client ownership, or identifier owned by another customer. Availability is not a reservation, so execute can still return 409 if the account changes first. The response deliberately does not reveal which internal check failed.
- The existing customer ID, login, password, profile, users, payroll data, cards, and integrations remain intact. Only the submitted billing address is refreshed.
- A normal paid partner order and subscription are created, and still-running trial entitlements are retired.
- The customer's ABN/IRD record is reused only when it is unclaimed or already belongs to that same customer; it is never transferred from another customer.
- The customer receives a mandatory informational notice naming your account and the claim reference. It says their account is intact, the claim did not grant payroll access, and OAuth still needs separate consent.
send_customer_welcome_email=falsedoes not suppress this notice. - Your account must have an actionable branding-compatible support contact before claiming. If a sending domain is configured, it must be verified first; the required notice never silently falls back to a Lightning Payroll sender for that domain.
- Cancelling the resulting order follows the normal 60-day order policy. It does not delete the customer, erase the relationship audit, or automatically “unclaim” the account.
9. Create a trial
Creates a client on a free one-month trial. Requires partner.checkout.write and an Idempotency-Key.
| Field path | Type | Required | Notes |
|---|---|---|---|
customer.first_name | string | Yes | 1-32 chars |
customer.last_name | string | Yes | 1-32 chars |
customer.email | Yes | Must be unique | |
customer.phone | string | Yes | 3-32 chars |
company.legal_name | string | Yes | 1-128 chars |
company.abn | string | Conditional | AU only. Exactly one of abn or ird_number must be sent. |
company.ird_number | string | Conditional | NZ only. Exactly one of abn or ird_number must be sent. |
billing_address | object | No | Optional for a trial, since nothing is billed. Same fields and rules as the order endpoint. When omitted, no address record is created and a warning says so. |
send_customer_welcome_email | boolean | No | Default true. Emails the client a branded link to set their password and start the trial. |
oauth_onboarding | object | No | Same block and behaviour as the order endpoint. Replaces the password-reset email with a single-use magic link into your consent screen. |
{
"customer": {
"first_name": "Alice",
"last_name": "Nguyen",
"email": "alice@example.com",
"phone": "+61 7 3000 0000"
},
"company": {
"legal_name": "Sunrise Hospitality Pty Ltd",
"abn": "10000000000"
}
}
Rules:
- The company's ABN or IRD number is claimed at this point, so no other reseller can start a competing trial for the same entity, and your later order for that company is accepted.
- A duplicate client email returns
409. - An ABN or IRD number already held by a different client returns
409. - Too many claim attempts in a minute returns
429. - The client's onboarding email carries your branding when you have white-label branding configured.
- You receive no invoice or confirmation email, because nothing was charged.
{
"scope_used": "partner.checkout.write",
"warnings": [],
"customer_id": 192601,
"address_id": null,
"subscription_id": 88214,
"subscription_trial_id": 26411,
"customer_email": "alice@example.com",
"company": {
"legal_name": "Sunrise Hospitality Pty Ltd",
"abn": "10000000000",
"ird_number": null,
"country_code": "AU",
"currency_code": "AUD"
},
"trial_start_date": "2026-07-31",
"trial_expiry": "2026-08-31",
"days_remaining": 31,
"site_mode": "au",
"emails_sent": true,
"idempotency_replayed": false
}
10. List trials
Paginated list of the trials you created, newest first. Accepts any partner checkout scope.
| Name | Type | Notes |
|---|---|---|
status | string | Optional filter: active, expired, or cancelled. Any other value returns 422. |
limit | integer | 1-100, default 25 |
offset | integer | Default 0 |
Each row returns subscription_trial_id, subscription_id, customer_id, customer_email, company_name, status, trial_start_date, trial_expiry, days_remaining, cancelled_at, converted, and converted_order_id.
statusisactivewhile the trial runs,cancelledif you ended it early, andexpiredonce it lapsed on its own.days_remainingis zero or negative once the trial has ended.convertedistrueonce you have placed a paid order for that client, andconverted_order_idnames it.- Trials that started on our own website are not listed, because they are not yours.
11. Cancel a trial
Ends a trial before its expiry date, for example when it was provisioned against the wrong entity. Requires partner.checkout.cancel or partner.checkout.write.
{
"subscription_trial_id": 26411,
"reason": "Provisioned against the wrong entity."
}
Rules:
- You may only cancel a trial you created (
403otherwise), and an unknown ID returns404. - Cancelling twice returns
409. - The trial subscription's expiry is set to yesterday, so the client's access ends immediately.
- The client record and the trial history are kept for audit; nothing is deleted.
- Cancelling does not release the company's claimed ABN or IRD number, because that record can carry live accounting-integration connections. If you need to re-provision the same company under a different email address, contact support.
{
"subscription_trial_id": 26411,
"subscription_id": 88214,
"customer_id": 192601,
"previous_trial_expiry": "2026-08-31",
"trial_expiry": "2026-07-30",
"cancelled_at": "2026-07-31T02:14:09",
"reason": "Provisioned against the wrong entity."
}
08Managing renewals and ending a subscription
Upgrades move a client up a tier immediately. Moving a client down a tier works differently: a mid-term downgrade is rejected at checkout, because a part-used higher plan is worth more than the lower plan being bought. What you can set is the plan a client renews onto.
For clients whose headcount moves seasonally, put them on a monthly billing cycle. Monthly does not change the downgrade rule, but it means a renewal comes around every month rather than once a year, so a client can move up when they need to and back down at the end of the month.
12. List upcoming renewals
Lists every client subscription you onboarded that has not yet renewed, oldest expiry first. Query parameters: limit (default 50, max 200), offset.
Each entry carries the current plan, is_monthly, auto_renew_on, will_auto_renew, renewal_state, the order_id and order_status_id of the order the subscription was sold on, and pending_renewal.
pending_renewal is null until a renewal order has been prepared. Those are created 30 days before expiry for annual subscriptions, and same-day as the previous renewal for monthly ones, so a monthly client has one you can change from their first renewal on. Check it is non-null, and that its editable flag is true, before calling the update below.
Whether a subscription will renew:
will_auto_renewistrueonly when the subscription will be renewed at expiry, subject to payment. Rely on it rather thanauto_renew_on.auto_renew_onis the client's preference. It is stored per customer, so it staystrueon a subscription that cannot renew.renewal_stategives the reason. The first that applies is reported:superseded,trial,lapsed(expiry has passed),no_order,order_cancelled(the order it was sold on is cancelled),order_not_complete,renewal_cancelled(its prepared renewal order is no longer pending),renewal_not_prepared(no renewal order exists and none will be prepared before expiry: an annual subscription inside 30 days of expiry; contact support if it should renew),auto_renew_off,will_renew. Treat a value you do not recognise as not renewing.- A subscription sold on an order that was later cancelled is still listed, with
renewal_stateorder_cancelled. Only a subscription sold on a complete order (order_status_id5) renews. editableisfalseunless the renewal order is still pending, the subscription was sold on a complete order, and it has not lapsed. Anauto_renew_offsubscription stays editable, since the same update can switch automatic renewal back on.
13. Change a pending renewal
Sets the plan a client renews onto, and whether they renew at all. Supply at least one field.
{
"renewal_product_id": 280,
"auto_renew_on": true,
"send_renewal_notification": true
}
Business rules:
- The subscription must be one your account onboarded, else
403. renewal_product_idedits the pending renewal order, so409if none exists yet or it is no longer pending.renewal_product_idis409on a subscription that will not renew: one sold on an order that is cancelled or not complete, or one that has lapsed. Its renewal line is never charged, so changing it achieves nothing. To put the client back on a plan, place a new order withend_customer_id. See "Ordering for an existing client".- The product must be one of your own subscription plans; add-ons, ad-hoc products and discount products are
422. - The line's price and tax resolve from the renewal order's billing country, falling back to your account's billing address;
409if neither resolves to AU or NZ. - The line is repriced at your negotiated reseller rate.
- Per-employee quantity is kept as it is, unless it falls below the client's
per_employee_minimum(default 15), in which case it is raised to that floor. auto_renew_onis stored against the end customer, so it applies to every subscription that customer holds. The response lists each affected subscription inauto_renew_affects_subscription_ids. It can still be set on a subscription that will not renew.- The response's
will_auto_renewandrenewal_statereport whether the subscription you passed will renew after the change. - A short notification is emailed to you whenever the line actually changes. Set
send_renewal_notificationtofalseto suppress it, for example when your own users drive the change.
No Idempotency-Key is needed. Editing an existing renewal line is idempotent by construction.
14. End a subscription early
Ends a client's paid subscription now, before its expiry date. The subscription's expiry is set to yesterday, so the client's access ends straight away and their data becomes read-only unless they hold another subscription. Requires partner.checkout.cancel or partner.checkout.write; a dry run also accepts partner.checkout.preview.
This is the only call that gives up paid time, so dry_run is required, with no default. Preview first: a dry run changes nothing and reports exactly what would end.
{
"subscription_id": 98321,
"dry_run": true,
"reason": "Client has stopped trading."
}
Choose the right call for what you want:
- Move the client to another plan: use
POST /api/partner-checkout/upgrades. An upgrade credits the unused value of the current subscription; ending it and placing a new order does not. - Stop the client renewing at the end of their term: set
auto_renew_ontofalsewithPATCH /api/partner-checkout/renewals/{subscription_id}. Access continues until the expiry date. - Reverse a sale placed in error: within 60 days of the order,
POST /api/partner-checkout/orders/cancelvoids the charge, and withend_subscription: truealso ends the access that order sold. Ending the subscription does not void the charge. - End a free trial: use
POST /api/partner-checkout/trials/cancel.
Rules:
- Nothing is refunded or credited.
refund_or_credit_issuedis alwaysfalse, andremaining_days_forfeitedcounts the paid days given up, from today through the old expiry. The order the subscription was sold on keeps its status and totals, and you are still billed for it. - The subscription must be the current one, sold on an order your account placed (
403otherwise). One that has been renewed or upgraded returns409naming the subscription that replaced it, which is the one to end. - A subscription sold on an order you already cancelled can be ended too, since nothing was billed for it. Cancelling an order without
end_subscription: trueleaves the client's access running until the expiry date. - Any subscription this one replaced through an upgrade or renewal that still had access running is ended with it, because its value was carried into this one. Each is listed in
ended_subscription_ids. - A pending renewal order is cancelled so the subscription is not renewed or billed again, and listed in
voided_renewal_order_ids. - A subscription sold on a base or both-mode upgrade order that can still be cancelled returns
409while the subscription the upgrade replaced still has time to run: ending it would end that one too and forfeit its time. Cancel the upgrade order withend_subscription: trueinstead, which restores the replaced subscription with its own expiry, then end that one if the client should lose access. - The client's add-ons lapse with the subscription, including add-ons sold on a separate add-on upgrade order. That order is not cancelled and stays billed, so
warningsnames it with the date its own cancellation window closes. Theirauto_renew_onpreference, account and data are left as they are, and no email is sent. - An audit note recording who ended it, the old expiry and your
reasonis added to the order's history. - A free trial returns
409with itssubscription_trial_id; an unknown ID returns404; one that has already ended returns409. A repeated call is therefore409, so noIdempotency-Keyis needed. warningstells you when the order is still inside its 60-day cancellation window, and, on a dry run, that an upgrade would credit the unused time.
{
"mode": "execute",
"dry_run": false,
"scope_used": "partner.checkout.cancel",
"subscription_id": 98321,
"customer_id": 192601,
"order_id": 192484,
"order_status_id": 5,
"previous_expiry": "2027-08-31",
"expiry": "2026-10-05",
"ended_at": "2026-10-06T02:14:09",
"remaining_days_forfeited": 330,
"refund_or_credit_issued": false,
"ended_subscription_ids": [98321],
"voided_renewal_order_ids": [],
"has_active_subscription": false,
"other_active_subscription_ids": [],
"reason": "Client has stopped trading.",
"warnings": []
}
A dry run returns the same shape with mode preview, ended_at null, and the values the call would produce.
09Looking up and updating a client
15. Resolve a client
Resolves a customer id into who that client is and whether they are currently subscribed. Requires any partner checkout scope.
Use it to turn the customerId on a webhook payload, or one you stored at onboarding, into a name, an email and the plans the client holds right now.
You can look up three kinds of client: clients you onboarded through partner checkout, clients you started on a free trial, and clients who have authorised your OAuth client.
The last case is the one that matters if your integration connects to customers who already subscribe rather than selling to them. Those clients have no order attributed to you, but a live authorisation is enough on its own. Any other customer id returns 404, whether or not it exists, so this cannot be used to discover ids you were not already given.
Cancelling a trial or an order, or ending a subscription, does not end the relationship, so those clients remain visible.
{
"customer_id": 12345,
"customer_name": "Alice Nguyen",
"company_name": "Acme Pty Ltd",
"email": "alice@example.com",
"has_active_subscription": true,
"subscriptions": [
{
"subscription_id": 987,
"product_id": 240,
"product_name": "Enterprise",
"expiry": "2027-03-01",
"status": "active",
"is_monthly": false,
"is_trial": false,
"auto_renew_on": true,
"quantity": 15,
"will_auto_renew": true,
"renewal_state": "will_renew",
"order_id": 45678,
"order_status_id": 5
}
]
}
Notes:
has_active_subscriptionis the same check that decides whether a client's data is writable. When it isfalse, reads keep working but writes return403, so testing this first separates a lapsed client from a genuine fault in your integration.subscriptionslists the current term of each subscription, latest expiry first. It is empty for a client who has never subscribed.statusbecomeslapsedon the day afterexpiry. A subscription expiring today is stillactive. It describes access, not renewal: anactivesubscription may still not renew.will_auto_renewsays whether the subscription will renew at expiry, andrenewal_statesays why not when it will not. Both carry the same meaning as on the renewals list in section 08. A subscription sold on an order that was later cancelled readsactiveuntil it expires, withrenewal_stateorder_cancelled.order_idandorder_status_ididentify the order the subscription was sold on, when your account placed it. They arenullon a trial, and on a subscription the client bought through another channel. On the latterrenewal_stateisnulltoo, since it is worked out from that order;will_auto_renewstill says whether it renews.is_trialmarks a free trial. A trial reads asactiveright up until it ends, so check this flag before treating a client as a paying customer.- A trial was never sold, so it has no order line behind it. On a trial,
product_id,product_name,quantityandis_monthlyare allnull. Treatis_trialas the reason those fields are missing, rather than expecting a product. auto_renew_onis the client's preference, stored against the customer rather than the subscription, so every entry carries the same value, including one that will not renew.
Access to this endpoint is not a substitute for the webhook stream. It answers "what is this client's state now", not "what changed".
16. Update a client's contact details
Corrects a client's first name, last name, phone number or login email. Requires partner.checkout.write. Send only the fields you want to change, and at least one.
{
"first_name": "Alice",
"last_name": "Nguyen",
"email": "alice.nguyen@example.com",
"phone": "+61 7 3000 0000"
}
{
"customer_id": 12345,
"first_name": "Alice",
"last_name": "Nguyen",
"email": "alice.nguyen@example.com",
"phone": "+61 7 3000 0000",
"changed_fields": ["email"],
"email_changed": true,
"previous_email_notified": true
}
Business rules:
- You can update clients you onboarded through partner checkout and clients you started on a free trial. Clients who have only authorised your OAuth client can be looked up above but not updated, because connecting your app does not hand you their account. Any other customer id returns
404, whether or not it exists. - An email already used by another account returns
409. A change of letter case alone is stored, but is not treated as a login change. changed_fieldslists only the fields whose stored value actually changed, so repeating a call is harmless. NoIdempotency-Keyis needed.
Changing email moves the client's login, immediately. When it changes:
- The previous address is emailed that the login changed.
previous_email_notifiedreports whether that notice was queued. - Any password reset or onboarding link already sent to the previous address stops working.
- The new address is emailed that it is now the login.
- No new onboarding link is sent. If the client has not onboarded yet, call
POST /api/partner-checkout/orders/{order_id}/resend-onboarding-link(endpoint 8) afterwards so the link reaches the new address. - If the client uses the desktop app, they must enter the new email under Tools > Licence Assistant before its licence check passes again.
10Idempotency
Idempotency only applies to order and upgrade execute requests where dry_run=false, and to trial creation.
Idempotency-Keyis required.- Maximum length is
255. - Same key plus same payload returns the original successful execute response with
idempotency_replayed=true. - A successful claim remains replayable with the same key and payload after its trial has been retired; do not generate a new key merely because a response timed out.
- Same key plus different payload returns
409. - Keys are shared across orders, upgrades and trials, so do not reuse an order's key for a trial or an upgrade.
- Cancelling an order or a trial, ending a subscription, and changing a renewal or a client's contact details take no key. Repeating a cancel or an end returns
409, and repeating a change alters nothing further.
11Pricing and promo behaviour
- Australia uses
AUD. - New Zealand uses
NZD. /optionscan show both partner pricing and mapped retail RRP values.- Preview and execute responses can include a welcome-offer promo discount on the base subscription line.
- Preview and execute responses return identical
order_linesandtotalsfor the same payload: preview is the price the partner agrees to, and execute persists those exact numbers.
Per-line GST and RRP disclosure
Every entry in order_lines includes the GST treatment that was applied and, where available, the retail comparison values:
| Field | Description |
|---|---|
tax_rate | Tax rate applied to this line. 0.1000 for AU GST, 0.0000 for NZ (GST-Free Export). |
currency_code | AUD or NZD. |
unit_price_ex_tax | Per-unit partner price excluding tax. AU base. |
unit_price_inc_tax | Per-unit partner price including tax. AU = ex_tax × 1.10. NZ = ex_tax (no NZ GST line). |
rrp_unit_price_ex_tax | Per-unit retail price excluding tax. null when no retail counterpart exists (e.g. Figtree, per-employee REF SKUs). Prorated when subscription_end_date is set. |
rrp_unit_price_inc_tax | Per-unit retail price including tax. Same GST rules as unit_price_inc_tax. |
rrp_line_total_inc_tax | RRP × quantity, including tax. |
LP-462 NZ pricing detail. Intellitron Pty Ltd is AU-GST-registered but not NZ-GST-registered, so sales to NZ are GST-Free Exports. To keep the numeric advertised price identical across AU and NZ, the NZ ex-tax price is uplifted by AU GST. NZ tax_rate stays 0.0000.
AU customer
- Pays
$1100 inc-taxfor the product
NZ customer
- Pays
$1100 ex-tax,$0 GST,$1100 inc-tax
The numeric advertised price is the same; the GST treatment differs.
12Subscription term alignment & prorating
Partners often need a new Lightning Payroll subscription to expire on the same date as the partner's own renewal cycle. Supply order.subscription_end_date to align the term and have the base subscription and recurring add-ons prorated to that window.
Rules:
subscription_end_datemust be strictly after today.- For annual base products it must be less than 365 days from today.
- For monthly base products it must be less than 30 days from today.
- Out-of-range values are rejected with
422. - A training session line, when requested via
order.include_training_session, is always charged at full retail; it is one-off labour, not a recurring service. - The aligned date is recorded as the subscription's exact expiry; the next term renews at full price under normal renewal rules.
Prorata formula
paid_days = (subscription_end_date - today) - (30 if add_free_trial_month else 0) factor = max(0, paid_days) / term_days # term_days = 365 annual, 30 monthly unit_price = round(base_after_reseller_discount * factor) # NZ uplift applied after
If paid_days <= 0 (e.g. a partner aligning a customer for a window smaller than the free trial month) the prorated lines are charged $0. A requested training session line remains at full price regardless.
Response fields
subscription_end_date: echoed when supplied,nullotherwise.prorata_factor: the multiplier applied to prorated lines,nullwhen no alignment was requested.warnings: includes a description of the prorata factor and any $0-floor or trial-month-consumed notes.
13Email behaviour
On successful execute:
- The authenticated reseller receives a billed-account confirmation email.
- If
oauth_onboardingwas supplied, the new customer receives a branded magic onboarding email (see "OAuth single-flow onboarding"); this replaces the password-reset welcome email. - Otherwise, if
send_customer_welcome_email=true, the new customer receives a branded onboarding email with a password-reset link. - An existing-trial claim always queues a separate mandatory customer notice, regardless of
send_customer_welcome_email. Its durable delivery status is operational state and is not represented byemails_sent.
If email sending fails, the order can still succeed and emails_sent will be false.
If onboarding or the OAuth connection needs recovery, call POST /api/partner-checkout/orders/{order_id}/resend-onboarding-link (endpoint 8). It safely chooses a fresh magic link or normal sign-in and reauthorization. Unlike execute, a recovery whose email fails returns 502 and issues no new magic link.
14Error reference
| Status | Typical causes |
|---|---|
| 400 | Invalid product selection, invalid add-on combination, missing execute idempotency key |
| 403 | Caller is not an eligible API admin reseller, missing required scope, or attempting to access another reseller's order or subscription |
| 404 | Order not found; subscription not found; client not found, or not one you may look up or update |
| 409 | Duplicate email after an explicit claim_existing_trial=false opt-out, ABN, or IRD; existing customer is not safely claimable; partner support/sending-domain settings are not ready for the mandatory claim notice; reused idempotency key with a different payload; an order with end_customer_id for a client who already has a live paid subscription (the message names one you sold and the endpoint to use instead; one sold elsewhere needs support), or a subscription still running on an order someone else placed (contact support); already-cancelled order; cancellation window expired; cancelling an order whose subscription, or the subscription its add-ons were added to, was later upgraded with credit (cancel the upgrade order first), a base or both-mode upgrade order without end_subscription: true, or an upgrade whose new subscription has since been renewed or upgraded again; end_subscription: true on an order whose subscription has since been renewed or upgraded into one that still runs (end that one with /subscriptions/end), or whose subscription carries add-ons sold on another order that is still billed (cancel that order first); an upgrade of a subscription that was already renewed or upgraded, expires today or has expired, or was sold on an order that is cancelled or not complete (it carries no upgrade credit, so place a new order with end_customer_id instead); a renewal product change on a subscription that will not renew, because it was sold on an order that is cancelled or not complete, has lapsed, or has no renewal order and will not get one (place a new order with end_customer_id instead); OAuth recovery for a customer not provisioned by this partner or one who already has a live connection; a client contact update to an email another account already uses; ending a subscription that is a free trial (use trials/cancel), has been renewed or upgraded into another subscription (end that one), is not linked to an order, has already ended, or was sold on an order that is neither complete nor cancelled, or a subscription sold on an upgrade order that can still be cancelled while the subscription it replaced still has time to run (cancel the upgrade order with end_subscription: true instead); the order or subscription changed while a cancel, upgrade, renewal change or end was in progress (the message says to retry the request) |
| 422 | Validation errors, missing required fields, unsupported extra fields, invalid ABN/IRD, invalid zone code |
| 500 | Unexpected server-side failure |
| 502 | Onboarding-link resend could not send the email; no new link was issued |
15Production checklist
Discover products and zones dynamically from the API.
Use availability checks when useful for preflight guidance; they are optional and never reserve a customer.
Always preview before executing.
Use Idempotency-Key on every execute request.
Persist customer_id, order_id, and subscription_id for support and reconciliation.
Treat warnings as actionable integration signals.
Handle emails_sent=false separately from order success.
For an email conflict, let checkout perform its automatic safe claim. Treat availability as optional guidance and never infer claimability from email.exists alone.
Use order lookup endpoints to reconcile retries and support cancellations.
When a cancelled order's client should lose access, send end_subscription: true with the cancel. Without it, access runs until the expiry date. If the cancel returns 409 because the subscription has since renewed or carries add-ons billed on another order, follow the message.
Read will_auto_renew and renewal_state to tell whether a subscription will renew. status describes access, and auto_renew_on is only the client's preference.
Handle the 409 for a client who already has a live paid subscription when ordering with end_customer_id: upgrade it, change its renewal, or end it first.
Preview POST /api/partner-checkout/subscriptions/end with dry_run=true before ending a subscription, and check remaining_days_forfeited, ended_subscription_ids and warnings, which name any add-on order that stays billed. Nothing is refunded. To undo an upgrade still inside its cancellation window, cancel the upgrade order instead.