Back to API documentation
# Partner Checkout Admin Endpoints Guide This guide documents Lightning Payroll's partner checkout API for reseller integrations where the authenticated caller is both: - an `api_admin` customer - in `customer_group_id = 9` It is written for external developers who cannot inspect the source code. ## Read This First Partner checkout builds on the platform-wide API admin and OAuth flows. Read these guides before implementing checkout: 1. `backend/docs/api_admin_setup_and_management_guide.md` 2. `backend/docs/oauth_authentication_guide.md` Those guides explain how to obtain access tokens, configure scopes, and authenticate as an API admin reseller. ## What 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 ## Base 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`. ## OAuth 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` | | Cancel order | `partner.checkout.cancel` or `partner.checkout.write` | | List or inspect past orders | Any partner checkout scope above | `openid` is still required on the OAuth authorization request. Legacy `openapi` remains accepted in the wider OAuth flow. ## Recommended Integration Sequence 1. `GET /api/partner-checkout/options` 2. `GET /api/partner-checkout/zones` 3. `GET /api/partner-checkout/availability?email=...&abn=...` or `...&irdNumber=...` 4. `POST /api/partner-checkout/orders` with `dry_run=true` 5. `POST /api/partner-checkout/orders` again with the same business data, `dry_run=false`, and an `Idempotency-Key` 6. Persist the returned `customer_id`, `order_id`, and `subscription_id` 7. Use `GET /api/partner-checkout/orders` and `GET /api/partner-checkout/orders/{order_id}` for reconciliation and support 8. Use `POST /api/partner-checkout/orders/cancel` only when the reseller needs to cancel a qualifying order Do not hardcode product IDs, add-on IDs, pricing, or zone codes. Discover them dynamically. ## Country, Identifier, and Address Rules - Only Australia (`AU`) and New Zealand (`NZ`) are supported. - Country is inferred from the company identifier: - send `company.abn` for Australia - send `company.ird_number` for New Zealand - You must provide exactly one of `abn` or `ird_number`. - `billing_address.country_code` is not accepted in requests. - `billing_address.company` is not accepted in requests. - `customer.password` is not accepted in requests. - `billing_address.zone_code` must match the inferred country. - `zone_code` is case-insensitive in the request and normalized to uppercase. - If a zone is invalid, the API returns `422` and includes the valid codes for that country in the error detail. ## Endpoint Reference ### 1. `GET /api/partner-checkout/options` 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 for `order.product_id` - `add_on_products`: optional or required add-ons - `mandatory_requirements.training_session_product_id`: mandatory training session product expected during signup - `mandatory_requirements.training_session_product_name` / `mandatory_requirements.training_session_prices`: display name and per-country (`AU`/`NZ`) retail pricing for the mandatory training session line, so the setup fee can be shown before previewing an order. These match the one-off `training_session` line returned by `preview`/`execute` (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 signups - `per_employee_minimum`: minimum quantity for per-employee base products - `show_per_employee_pricing`: whether this reseller can use per-employee plans - `prices`: country-specific partner pricing and, where available, retail RRP comparison values ### 2. `GET /api/partner-checkout/zones` Use this endpoint to fetch the supported countries and valid `zone_code` values for billing addresses. The response currently includes: - `AU` - `NZ` Use the returned `zone.code` values as `billing_address.zone_code`. ### 3. `GET /api/partner-checkout/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 behavior: - emails are trimmed and lowercased - ABNs are digit-normalized before validation - IRD numbers are digit-normalized before validation ### 4. `POST /api/partner-checkout/orders` 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. | | `send_customer_welcome_email` | boolean | No | Default `false`. Sends branded onboarding email with reset link when true. | | `customer.first_name` | string | Yes | 1-32 chars | | `customer.last_name` | string | Yes | 1-32 chars | | `customer.email` | 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.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 trial month. | | `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.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. - Unknown fields are rejected with `422`. - Base `product_id` must 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_qty` may 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`; use `extra_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. - A mandatory training session product is always added to the order. - When `subscription_end_date` is supplied, the base subscription and recurring add-ons are prorated; the training session 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 = true` - `customer_id`, `address_id`, `order_id`, and `subscription_id` are `null` - `order_lines` shows the base subscription, training session line, and any add-ons - `totals` shows subtotal, tax, total, and any welcome-offer promo discount - `warnings` describes non-fatal conditions such as capped extra-company quantity or the free trial month #### Execute response When `dry_run=false`, the API creates: - a new customer - a billing/shipping address - a company record with the ABN or employer IRD number - an order with `payment_code = "partner_checkout_billed_account"` - order totals and order products - a subscription and any subscription add-ons - a `post_pay` record for billed-account invoicing - order history - optional promo assignment - optional subscription-trial record when the free extra month is used Important execute response fields: - `customer_id` - `address_id` - `order_id` - `subscription_id` - `order_status_id` - `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: 1. You send `dry_run=false` with `oauth_onboarding.redirect_uri` (and optionally `scope` / `state`). 2. The new customer is emailed a **single-use, 24-hour magic link**. Opening it signs them in **without a password** and drops them straight onto your OAuth consent screen. 3. With one click of consent, their browser is redirected to your `redirect_uri` with `?code=...&state=...`. 4. 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_uri` must exactly match one of its registered redirect URIs, and `scope` must include `payroll.write`. A `dry_run=true` request 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 24 hours. Re-provisioning (a new order) issues a fresh link. - `oauth_onboarding` replaces the password-reset welcome email; `send_customer_welcome_email` is ignored when it is supplied. ### 5. `POST /api/partner-checkout/orders/cancel` Use this endpoint to cancel an order previously created by the authenticated reseller. Rules: - reseller must own the order - order must not already be cancelled - cancellation is only allowed within 60 days of the created customer's `date_added` - `partner.checkout.cancel` is preferred, but `partner.checkout.write` is also accepted ### 6. `GET /api/partner-checkout/orders/{order_id}` 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` - ascending `history[]` ### 7. `GET /api/partner-checkout/orders` Lists only the orders created by the authenticated reseller, newest first. Query parameters: - `status` - `limit` - `offset` ## Idempotency Idempotency only applies to execute requests where `dry_run=false`. - `Idempotency-Key` is required - maximum length is `255` - same key plus same payload returns the original successful execute response with `idempotency_replayed=true` - same key plus different payload returns `409` ## Pricing and Promo Behavior - Australia uses `AUD` - New Zealand uses `NZD` - `/options` can 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_lines` and `totals` for the same payload — preview is the price the partner agrees to; 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`. This means: - An AU customer paying `$1100 inc-tax` for the same product - An NZ customer pays `$1100 ex-tax`, `$0 GST`, `$1100 inc-tax` The numeric advertised price is the same; the GST treatment differs. ## Subscription 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_date` must 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`. - The mandatory training session line 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**. The training session line remains at full price regardless. ### Response fields - `subscription_end_date` — echoed when supplied; `null` otherwise - `prorata_factor` — the multiplier applied to prorated lines; `null` when no alignment was requested - `warnings` — includes a description of the prorata factor and any $0-floor or trial-month-consumed notes ## Email Behavior On successful execute: - the authenticated reseller receives a billed-account confirmation email - if `oauth_onboarding` was 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 If email sending fails, the order can still succeed and `emails_sent` will be `false`. ## Error 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 | | `404` | Order not found | | `409` | Duplicate email, ABN, or IRD; reused idempotency key with a different payload; already-cancelled order; cancellation window expired | | `422` | Validation errors, missing required fields, unsupported extra fields, invalid ABN/IRD, invalid zone code | | `500` | Unexpected server-side failure | ## Production Checklist 1. Discover products and zones dynamically from the API. 2. Run availability checks before previewing or executing. 3. Always preview before executing. 4. Use `Idempotency-Key` on every execute request. 5. Persist `customer_id`, `order_id`, and `subscription_id` for support and reconciliation. 6. Treat `warnings` as actionable integration signals. 7. Handle `emails_sent=false` separately from order success. 8. Use order lookup endpoints to reconcile retries and support cancellations.