Download OpenAPI specification:Download
The Laabam One API lets your systems and approved third parties read and write data in your company's Laabam One account — invoices, customers, products, payments — and send messages over SMS, WhatsApp and email.
Every request authenticates with two headers — X-Client-Id and
X-Client-Secret, created in Settings → API Keys. See the
Authentication and Security guides
for the full details. Each operation below is marked x-status: available
(callable today) or x-status: planned (contract defined, not yet built).
60 requests per minute per key, unless a different figure is set on the key itself — the current value is shown on the key in Settings → API Keys, and you can ask us to raise it.
The window is per X-Client-Id, not per IP, so an embedded widget calling
from many end-user browsers shares one budget, and one key can never
exhaust another's. Every 429 carries Retry-After (seconds),
X-RateLimit-Limit and X-RateLimit-Remaining — retry on the header, not
on a guess.
List endpoints are cursor-paged: ?limit= (1–100, default 25) and
?cursor=, with the next cursor in paging.next_cursor and
paging.has_more telling you whether to ask again. Stop when has_more is
false; do not construct a cursor yourself.
Send an Idempotency-Key header (any UUID you generate) on every POST.
The first 2xx is cached against that key for 24 hours and replayed on a
repeat, so a timeout you retry cannot create a second invoice, work item or
payment. A handful of operations do not support it and say so explicitly.
✅ Read, create, send & void available (create posts the Sales JE + moves stock; INR). Update (PATCH draft) planned.
| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| limit | integer [ 1 .. 100 ] Default: 25 Page size (1–100). |
| cursor | string Opaque cursor from the previous response's paging.next_cursor. |
| status | string Enum: "unpaid" "partial" "paid" Filters on the invoice's payment_status (not the |
| customer_id | string |
{- "data": [
- {
- "id": "inv_5c1a",
- "number": "INV/2025-26/041",
- "status": "unpaid",
- "customer_id": "string",
- "currency": "INR",
- "base_currency": "INR",
- "base_currency_rate": 1,
- "base_total": "3540.00",
- "base_due": "3540.00",
- "subtotal": "3000.00",
- "tax_amount": "540.00",
- "round_off": "0.00",
- "total": "3540.00",
- "due_amount": "3540.00",
- "invoice_date": "2019-08-24",
- "due_date": "2019-08-24",
- "items": [
- {
- "product_id": "string",
- "quantity": 0,
- "unit_price": "1500.00",
- "tax_rate": 18,
- "amount": "3540.00"
}
], - "created_at": "2019-08-24T14:15:22Z"
}
], - "paging": {
- "next_cursor": "string",
- "has_more": true
}
}Creates an invoice, posts the Sales journal entry (DR receivable, CR sales + GST split) and deducts stock — the exact same accounting engine as the in-app flow. Totals, tax and round-off are computed server-side and are authoritative (client-sent totals are ignored). Each line requires item_name, quantity, unit, unit_price and tax_rate. INR only in v1 (recurring, attachments and pay-at-create are app-only).
| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| Idempotency-Key | string <uuid> A unique UUID so retries don't double‑create/charge/send. Cached 24h. |
| customer_id required | string |
| invoice_date required | string <date> |
| due_date | string <date> |
required | Array of objects (InvoiceItemCreate) non-empty |
| document_type | string Default: "invoice" Enum: "invoice" "quotation" "estimate" "proforma_invoice" "sales_order" |
| invoice_number | string <= 50 characters Auto-generated when omitted. |
| reference_number | string <= 255 characters |
| gst_invoice_type | string Enum: "regular" "bill_of_supply" "export_sez" |
| reverse_charge | string Enum: "Y" "N" |
| place_of_supply | string <= 100 characters India: the buyer's state — a 2-digit GST state code ("33"), "33 - Tamil Nadu" or the state name. Decides CGST + SGST or IGST. |
| currency | string = 3 characters ISO 4217. Omit for the company's own currency. |
| base_currency_rate | number > 0 Required with a foreign |
| charged_total | number >= 0.01 The amount already collected, tax included, in the invoice's currency. Holds the total to it instead of the company's round-off, so a payment of that amount settles the invoice exactly. |
| terms | string |
| notes | string |
{- "customer_id": "string",
- "invoice_date": "2019-08-24",
- "due_date": "2019-08-24",
- "items": [
- {
- "item_name": "string",
- "quantity": 0.01,
- "unit": "pcs",
- "unit_price": 1500,
- "tax_rate": 18,
- "product_id": 0,
- "description": "string",
- "hsn_code": "string",
- "discount_percent": 100
}
], - "document_type": "invoice",
- "invoice_number": "string",
- "reference_number": "string",
- "gst_invoice_type": "regular",
- "reverse_charge": "Y",
- "place_of_supply": "Karnataka",
- "currency": "USD",
- "base_currency_rate": 88.5,
- "charged_total": 0.01,
- "terms": "string",
- "notes": "string"
}{- "data": {
- "id": "inv_5c1a",
- "number": "INV/2025-26/041",
- "status": "unpaid",
- "customer_id": "string",
- "currency": "INR",
- "base_currency": "INR",
- "base_currency_rate": 1,
- "base_total": "3540.00",
- "base_due": "3540.00",
- "subtotal": "3000.00",
- "tax_amount": "540.00",
- "round_off": "0.00",
- "total": "3540.00",
- "due_amount": "3540.00",
- "invoice_date": "2019-08-24",
- "due_date": "2019-08-24",
- "items": [
- {
- "product_id": "string",
- "quantity": 0,
- "unit_price": "1500.00",
- "tax_rate": 18,
- "amount": "3540.00"
}
], - "created_at": "2019-08-24T14:15:22Z"
}
}| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| id required | string Opaque resource identifier. |
{- "data": {
- "id": "inv_5c1a",
- "number": "INV/2025-26/041",
- "status": "unpaid",
- "customer_id": "string",
- "currency": "INR",
- "base_currency": "INR",
- "base_currency_rate": 1,
- "base_total": "3540.00",
- "base_due": "3540.00",
- "subtotal": "3000.00",
- "tax_amount": "540.00",
- "round_off": "0.00",
- "total": "3540.00",
- "due_amount": "3540.00",
- "invoice_date": "2019-08-24",
- "due_date": "2019-08-24",
- "items": [
- {
- "product_id": "string",
- "quantity": 0,
- "unit_price": "1500.00",
- "tax_rate": 18,
- "amount": "3540.00"
}
], - "created_at": "2019-08-24T14:15:22Z"
}
}| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| id required | string Opaque resource identifier. |
| customer_id required | string |
| invoice_date required | string <date> |
| due_date | string <date> |
required | Array of objects (InvoiceItemCreate) non-empty |
| document_type | string Default: "invoice" Enum: "invoice" "quotation" "estimate" "proforma_invoice" "sales_order" |
| invoice_number | string <= 50 characters Auto-generated when omitted. |
| reference_number | string <= 255 characters |
| gst_invoice_type | string Enum: "regular" "bill_of_supply" "export_sez" |
| reverse_charge | string Enum: "Y" "N" |
| place_of_supply | string <= 100 characters India: the buyer's state — a 2-digit GST state code ("33"), "33 - Tamil Nadu" or the state name. Decides CGST + SGST or IGST. |
| currency | string = 3 characters ISO 4217. Omit for the company's own currency. |
| base_currency_rate | number > 0 Required with a foreign |
| charged_total | number >= 0.01 The amount already collected, tax included, in the invoice's currency. Holds the total to it instead of the company's round-off, so a payment of that amount settles the invoice exactly. |
| terms | string |
| notes | string |
{- "customer_id": "string",
- "invoice_date": "2019-08-24",
- "due_date": "2019-08-24",
- "items": [
- {
- "item_name": "string",
- "quantity": 0.01,
- "unit": "pcs",
- "unit_price": 1500,
- "tax_rate": 18,
- "product_id": 0,
- "description": "string",
- "hsn_code": "string",
- "discount_percent": 100
}
], - "document_type": "invoice",
- "invoice_number": "string",
- "reference_number": "string",
- "gst_invoice_type": "regular",
- "reverse_charge": "Y",
- "place_of_supply": "Karnataka",
- "currency": "USD",
- "base_currency_rate": 88.5,
- "charged_total": 0.01,
- "terms": "string",
- "notes": "string"
}{- "data": {
- "id": "inv_5c1a",
- "number": "INV/2025-26/041",
- "status": "unpaid",
- "customer_id": "string",
- "currency": "INR",
- "base_currency": "INR",
- "base_currency_rate": 1,
- "base_total": "3540.00",
- "base_due": "3540.00",
- "subtotal": "3000.00",
- "tax_amount": "540.00",
- "round_off": "0.00",
- "total": "3540.00",
- "due_amount": "3540.00",
- "invoice_date": "2019-08-24",
- "due_date": "2019-08-24",
- "items": [
- {
- "product_id": "string",
- "quantity": 0,
- "unit_price": "1500.00",
- "tax_rate": 18,
- "amount": "3540.00"
}
], - "created_at": "2019-08-24T14:15:22Z"
}
}Voids (soft-deletes) the invoice and reverses all its side-effects — its journal entries, any payments + their JEs, and restores deducted stock — the same reversal as the app. Returns a summary of what was reversed.
| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| id required | string Opaque resource identifier. |
{- "data": {
- "id": "string",
- "voided": true,
- "reversal": { }
}
}Renders the invoice PDF (using your configured print template) and queues it for email to the customer — the same render the in‑app "Send" button uses. Recipients default to the invoice customer's email; override with email or emails[] (max 10). A draft invoice is marked as sent. Only channel: email is available today; WhatsApp invoice delivery is planned.
| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| id required | string Opaque resource identifier. |
| channel | string Default: "email" Value: "email" |
string <email> Single recipient (overrides the customer email). | |
| emails | Array of strings <email> [ items <email > ] Multiple recipients (max 10). |
| message | string <= 1000 characters Optional note included in the email. |
{- "channel": "email",
- "email": "user@example.com",
- "emails": [
- "user@example.com"
], - "message": "string"
}{- "data": {
- "id": "string",
- "channel": "email",
- "status": "queued",
- "invoice_id": "string",
- "to": [
- "string"
], - "queued_count": 0,
- "created_at": "2019-08-24T14:15:22Z"
}
}✅ Read, create, update & delete available (create also opens the Sundry Debtors ledger account).
| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| limit | integer [ 1 .. 100 ] Default: 25 Page size (1–100). |
| cursor | string Opaque cursor from the previous response's paging.next_cursor. |
| q | string Free‑text search (name / email / phone). |
{- "data": [
- {
- "id": "42",
- "customer_number": "string",
- "name": "string",
- "email": "user@example.com",
- "phone": "string",
- "gstin": "string",
- "pan": "string",
- "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postal_code": "string",
- "country": "IN"
}, - "currency": "string",
- "is_active": true,
- "created_at": "2019-08-24T14:15:22Z"
}
], - "paging": {
- "next_cursor": "string",
- "has_more": true
}
}Creates a customer and its Sundry Debtors ledger account (same path as the UI). name, state and pincode are required. opening_balance and profile images are NOT settable via the API (the opening-balance ledger entry remains a UI-only action).
| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| Idempotency-Key | string <uuid> A unique UUID so retries don't double‑create/charge/send. Cached 24h. |
| name required | string <= 255 characters |
string <email> <= 255 characters | |
| phone | string <= 20 characters |
| phone_country_code | string |
| alternate_phone | string <= 20 characters |
| address | string |
| address_label | string |
| area | string |
| city | string |
| state required | string |
| pincode required | string |
| country | string <= 2 characters ISO alpha-2. |
| gst_number | string <= 15 characters |
| gst_type | string |
| legal_name | string |
| pan_number | string <= 10 characters |
| customer_type | string Enum: "individual" "business" |
| currency_code | string = 3 characters |
| discount | number [ 0 .. 100 ] |
| credit_limit | number >= 0 |
| website_url | string <uri> |
| also_vendor | boolean |
| is_active | boolean |
| notes | string |
{- "name": "string",
- "email": "user@example.com",
- "phone": "string",
- "phone_country_code": "string",
- "alternate_phone": "string",
- "address": "string",
- "address_label": "string",
- "area": "string",
- "city": "string",
- "state": "string",
- "pincode": "string",
- "country": "st",
- "gst_number": "string",
- "gst_type": "string",
- "legal_name": "string",
- "pan_number": "string",
- "customer_type": "individual",
- "currency_code": "str",
- "discount": 100,
- "credit_limit": 0,
- "also_vendor": true,
- "is_active": true,
- "notes": "string"
}{- "data": {
- "id": "42",
- "customer_number": "string",
- "name": "string",
- "email": "user@example.com",
- "phone": "string",
- "gst_number": "string",
- "state": "string",
- "pincode": "string",
- "currency_code": "string",
- "is_active": true,
- "also_vendor": true,
- "account_id": 0,
- "vendor_account_id": 0,
- "created_at": "2019-08-24T14:15:22Z"
}
}| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| id required | string Opaque resource identifier. |
{- "data": {
- "id": "42",
- "customer_number": "string",
- "name": "string",
- "email": "user@example.com",
- "phone": "string",
- "gstin": "string",
- "pan": "string",
- "address": {
- "line1": "string",
- "city": "string",
- "state": "string",
- "postal_code": "string",
- "country": "IN"
}, - "currency": "string",
- "is_active": true,
- "created_at": "2019-08-24T14:15:22Z"
}
}Partial update — send only the fields you want to change. Renaming keeps the linked ledger account names in sync. PUT is accepted as well as PATCH; both behave as a partial update, so a PUT will not blank the fields you leave out.
| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| id required | string Opaque resource identifier. |
| name required | string <= 255 characters |
string <email> <= 255 characters | |
| phone | string <= 20 characters |
| phone_country_code | string |
| alternate_phone | string <= 20 characters |
| address | string |
| address_label | string |
| area | string |
| city | string |
| state required | string |
| pincode required | string |
| country | string <= 2 characters ISO alpha-2. |
| gst_number | string <= 15 characters |
| gst_type | string |
| legal_name | string |
| pan_number | string <= 10 characters |
| customer_type | string Enum: "individual" "business" |
| currency_code | string = 3 characters |
| discount | number [ 0 .. 100 ] |
| credit_limit | number >= 0 |
| website_url | string <uri> |
| also_vendor | boolean |
| is_active | boolean |
| notes | string |
{- "name": "string",
- "email": "user@example.com",
- "phone": "string",
- "phone_country_code": "string",
- "alternate_phone": "string",
- "address": "string",
- "address_label": "string",
- "area": "string",
- "city": "string",
- "state": "string",
- "pincode": "string",
- "country": "st",
- "gst_number": "string",
- "gst_type": "string",
- "legal_name": "string",
- "pan_number": "string",
- "customer_type": "individual",
- "currency_code": "str",
- "discount": 100,
- "credit_limit": 0,
- "also_vendor": true,
- "is_active": true,
- "notes": "string"
}{- "data": {
- "id": "42",
- "customer_number": "string",
- "name": "string",
- "email": "user@example.com",
- "phone": "string",
- "gst_number": "string",
- "state": "string",
- "pincode": "string",
- "currency_code": "string",
- "is_active": true,
- "also_vendor": true,
- "account_id": 0,
- "vendor_account_id": 0,
- "created_at": "2019-08-24T14:15:22Z"
}
}Soft-deletes the customer and removes its debtor account. Blocked with 409 if the customer has invoices, payments or posted ledger transactions. If the record is also a vendor, only the customer role is removed (vendor data is retained).
| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| id required | string Opaque resource identifier. |
{- "data": {
- "id": "string",
- "deleted": true
}
}| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| limit | integer [ 1 .. 100 ] Default: 25 Page size (1–100). |
| cursor | string Opaque cursor from the previous response's paging.next_cursor. |
| q | string |
| category_id | string |
{- "data": [
- {
- "id": "42",
- "name": "string",
- "sku": "string",
- "hsn_code": "string",
- "sale_price": 0,
- "mrp": 0,
- "gst_rate": 0,
- "track_inventory": true,
- "current_stock": 0,
- "created_at": "2019-08-24T14:15:22Z"
}
], - "paging": {
- "next_cursor": "string",
- "has_more": true
}
}Creates a product. If track_stock is true and opening_stock > 0, an opening stock movement (and a batch when is_batch_tracked) is recorded. name and unit_id are required. Images, variants and bundles are not settable via the API.
| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| name required | string |
| unit_id required | integer Required. Must exist in the company's units. |
| type | string Default: "product" Enum: "product" "service" |
| invoice_name | string Defaults to |
| sku | string <= 100 characters Must be unique. |
| barcode | string <= 100 characters Must be unique. |
| category_id | integer |
| brand_id | integer |
| description | string |
| purchase_price | number >= 0 |
| selling_price | number >= 0 |
| mrp_price | number >= 0 |
| selling_price_tax_mode | string Enum: "exclusive" "inclusive" |
| gst_rate | number [ 0 .. 100 ] |
| hsn_code | string <= 20 characters |
| sac_code | string <= 20 characters |
| status | string Default: "active" Enum: "active" "inactive" |
| track_stock | boolean Create only — not updatable via PATCH. |
| opening_stock | number |
| min_stock | number |
| max_stock | number |
| low_stock_alert | boolean |
| is_batch_tracked | boolean |
| is_expiry_tracked | boolean |
{- "name": "string",
- "unit_id": 0,
- "type": "product",
- "invoice_name": "string",
- "sku": "string",
- "barcode": "string",
- "category_id": 0,
- "brand_id": 0,
- "description": "string",
- "purchase_price": 0,
- "selling_price": 0,
- "mrp_price": 0,
- "selling_price_tax_mode": "exclusive",
- "gst_rate": 18,
- "hsn_code": "string",
- "sac_code": "string",
- "status": "active",
- "track_stock": true,
- "opening_stock": 0,
- "min_stock": 0,
- "max_stock": 0,
- "low_stock_alert": true,
- "is_batch_tracked": true,
- "is_expiry_tracked": true
}{- "data": {
- "id": "42",
- "name": "string",
- "invoice_name": "string",
- "sku": "string",
- "unit_id": 0,
- "purchase_price": 0,
- "selling_price": 0,
- "mrp_price": 0,
- "gst_rate": 0,
- "track_stock": true,
- "status": "string",
- "current_stock": 0,
- "created_at": "2019-08-24T14:15:22Z"
}
}| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| id required | string Opaque resource identifier. |
{- "data": {
- "id": "42",
- "name": "string",
- "sku": "string",
- "hsn_code": "string",
- "sale_price": 0,
- "mrp": 0,
- "gst_rate": 0,
- "track_inventory": true,
- "current_stock": 0,
- "created_at": "2019-08-24T14:15:22Z"
}
}Partial update of scalar fields (name, prices, tax/HSN, sku, status, …). Stock and track_stock are NOT updatable here — use the stock-adjustments endpoint to move stock. PUT is accepted as well as PATCH; both behave as a partial update, so a PUT will not blank the fields you leave out.
| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| id required | string Opaque resource identifier. |
| name | string |
| unit_id | integer |
| invoice_name | string |
| sku | string <= 100 characters |
| barcode | string <= 100 characters |
| category_id | integer |
| brand_id | integer |
| description | string |
| purchase_price | number >= 0 |
| selling_price | number >= 0 |
| mrp_price | number >= 0 |
| selling_price_tax_mode | string Enum: "exclusive" "inclusive" |
| gst_rate | number [ 0 .. 100 ] |
| hsn_code | string <= 20 characters |
| sac_code | string <= 20 characters |
| status | string Enum: "active" "inactive" |
| min_stock | number |
| max_stock | number |
| low_stock_alert | boolean |
{- "name": "string",
- "unit_id": 0,
- "invoice_name": "string",
- "sku": "string",
- "barcode": "string",
- "category_id": 0,
- "brand_id": 0,
- "description": "string",
- "purchase_price": 0,
- "selling_price": 0,
- "mrp_price": 0,
- "selling_price_tax_mode": "exclusive",
- "gst_rate": 100,
- "hsn_code": "string",
- "sac_code": "string",
- "status": "active",
- "min_stock": 0,
- "max_stock": 0,
- "low_stock_alert": true
}{- "data": {
- "id": "42",
- "name": "string",
- "invoice_name": "string",
- "sku": "string",
- "unit_id": 0,
- "purchase_price": 0,
- "selling_price": 0,
- "mrp_price": 0,
- "gst_rate": 0,
- "track_stock": true,
- "status": "string",
- "current_stock": 0,
- "created_at": "2019-08-24T14:15:22Z"
}
}Records a manual stock-adjustment movement and moves the product's current stock.
| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| id required | string Opaque resource identifier. |
| adjustment_type required | string Enum: "increase" "decrease" |
| quantity required | number >= 0.01 Always positive; direction is set by adjustment_type. |
| reason | string <= 100 characters |
| notes | string <= 500 characters |
{- "adjustment_type": "increase",
- "quantity": 0.01,
- "reason": "string",
- "notes": "string"
}{- "data": {
- "id": "string",
- "movement_id": "string",
- "adjustment_type": "string",
- "quantity": 0,
- "current_stock": 0,
- "created_at": "2019-08-24T14:15:22Z"
}
}| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| limit | integer [ 1 .. 100 ] Default: 25 Page size (1–100). |
| cursor | string Opaque cursor from the previous response's paging.next_cursor. |
| invoice_id | string |
| customer_id | string |
{- "data": [
- {
- "id": "42",
- "payment_number": "string",
- "invoice_id": "string",
- "customer_id": "string",
- "amount": 3540,
- "currency": "string",
- "reference": "string",
- "status": "string",
- "payment_date": "2019-08-24",
- "created_at": "2019-08-24T14:15:22Z"
}
], - "paging": {
- "next_cursor": "string",
- "has_more": true
}
}Records a payment and posts the receipt journal entry (DR bank/cash, CR the customer's receivable) as the in-app "Record Payment" does, then updates the invoice's paid/due/status. payment_mode_id chooses the bank/cash account — list options via GET /payments/{company}/methods.
Currency: for an invoice in the company's own currency, amount is in that currency. For an invoice raised in another currency (currency + base_currency_rate on create), amount is what was received in the company's own currency (for example the rupees a gateway settled); the invoice is settled in its own currency and any exchange difference is booked, as the in-app Record Payment does. base_currency_rate may give the rate on the payment date. TDS is not supported on a foreign-currency invoice.
The journal entry is best-effort: if the cash/bank account, the receivable account or the financial year can't be resolved, the payment is still recorded and journal_entry_id comes back null.
| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| Idempotency-Key | string <uuid> A unique UUID so retries don't double‑create/charge/send. Cached 24h. |
| invoice_id required | string |
| amount required | number >= 0.01
|
| payment_date required | string <date> |
| payment_mode_id required | integer From GET /payments/{company}/methods. |
| tds_amount | number >= 0 TDS withheld by the buyer. Counts toward settling the invoice alongside |
| tds_section | string <= 40 characters |
| reference_number | string <= 100 characters |
| notes | string <= 500 characters |
| base_currency_rate | number > 0 Foreign-currency invoices only: how many units of the company's own currency one unit of the invoice's currency was worth on the payment date. Defaults to the invoice's rate. |
{- "invoice_id": "string",
- "amount": 0.01,
- "payment_date": "2019-08-24",
- "payment_mode_id": 0,
- "tds_amount": 0,
- "tds_section": "string",
- "reference_number": "string",
- "notes": "string",
- "base_currency_rate": 88.5
}{- "data": {
- "id": "42",
- "payment_number": "string",
- "invoice_id": "string",
- "amount": 0,
- "payment_date": "2019-08-24",
- "status": "string",
- "journal_entry_id": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "invoice": {
- "id": "string",
- "paid_amount": 0,
- "due_amount": 0,
- "status": "string",
- "payment_status": "string"
}
}
}Active payment modes for the company — use the returned id as payment_mode_id when recording a payment.
| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
{- "data": [
- {
- "id": "string",
- "name": "string",
- "mode_type": "string"
}
]
}| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| id required | string Opaque resource identifier. |
{- "data": {
- "id": "42",
- "payment_number": "string",
- "invoice_id": "string",
- "customer_id": "string",
- "amount": 3540,
- "currency": "string",
- "reference": "string",
- "status": "string",
- "payment_date": "2019-08-24",
- "created_at": "2019-08-24T14:15:22Z"
}
}Reverses the payment — voids its journal entry and recomputes the invoice's paid/due/status. Full reversal only (partial refunds are not supported yet; no request body is read).
⚠️ The payment record is removed, not flagged. After a refund GET /payments/{company}/{id} returns 404, and refunding the same id twice also returns 404 — there is no refunded: true state to read back.
| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| id required | string Opaque resource identifier. |
| Idempotency-Key | string <uuid> A unique UUID so retries don't double‑create/charge/send. Cached 24h. |
{- "data": {
- "id": "string",
- "refunded": true
}
}✅ SMS, WhatsApp & Email available. Security‑sensitive — dedicated send scopes, lower rate limits and a daily cap. Path carries the company slug. Everything you need to configure a send — slug, endpoint, pe_id, and each approved sender_id with its template_ids and exact message text — is in Settings → API Keys → “Integration details”, with copy buttons. For India, read the DLT notes on Send an SMS first: a mismatched template is dropped by the carrier while this API still returns 202.
Sends an SMS via the company's gateway. Requires the messages:sms:send scope. Subject to the messaging rate limit + daily cap.
India (DLT) — read this before you debug for an afternoon. Pass an approved sender_id plus its registered template_id (and pe_id), and make message match the approved template text exactly — including punctuation and any sender suffix (e.g. ... -ACMEID). Variables occupy the {#var#} slots. sender_id and template_id must be a matched pair: the template is registered against one sender.
⚠️ A mismatch fails silently. If message does not match the registered template, or template_id is missing/wrong, the carrier DROPS the message — but this endpoint still returns 202. It will look like it worked while nothing is ever delivered. 202 means accepted, not delivered (lifecycle: queued → submitted → delivered); confirm real delivery with GET /messaging/{company}/messages/{id}.
Where to get all four values. Open Settings → API Keys → “Integration details”. It lists, with copy buttons: the company slug, the full endpoint URL, your pe_id, and every approved sender ID with its own templates nested underneath — each showing the template_id, the exact registered message text, the variable count and the character limit. Only approved rows appear, because anything else is dropped by the carrier.
Because the templates are grouped under their sender there, you can read off a valid sender_id + template_id pair directly instead of correlating two separate lists and guessing. A sender with no templates listed cannot send anything yet. The same data lives in Marketing → SMS Gateway (DLT Registration / Sender IDs / Templates) if you prefer to manage it there.
Copy the message text from that panel and substitute only the {#var#} slots — retyping it is how the trailing sender suffix gets lost.
| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| to required | string Recipient number, digits only (8–15). |
| message required | string <= 1000 characters |
| sender_id | string <= 11 characters An APPROVED sender ID, copied from Settings → API Keys → Integration details. Must be the sender the |
| template_id | string <= 64 characters Registered DLT template id, copied from under its sender in Settings → API Keys → Integration details. Omit it, or pair it with a different sender, and Indian carriers drop the SMS silently while this API still returns 202. |
| pe_id | string <= 64 characters DLT principal entity (PE) id, shown in Settings → API Keys → Integration details. One per company — the same value for every sender and template. |
| type | string Default: "transactional" Enum: "transactional" "promotional" |
{- "to": "919800000000",
- "message": "Your OTP is 123456",
- "sender_id": "ACMEID",
- "template_id": "1007487766981813506",
- "pe_id": "1001609937061127455",
- "type": "transactional"
}{- "data": {
- "id": "msg_7b3c",
- "channel": "sms",
- "status": "queued",
- "to": "string",
- "sent_at": "2019-08-24T14:15:22Z",
- "delivered_at": "2019-08-24T14:15:22Z",
- "read_at": "2019-08-24T14:15:22Z",
- "failed_at": "2019-08-24T14:15:22Z",
- "error": "string",
- "created_at": "2019-08-24T14:15:22Z"
}
}Sends via the company's Meta Cloud API provider. Business‑initiated messages must use a Meta‑APPROVED template; free text is only delivered inside an open 24‑hour customer‑service window. Requires the messages:whatsapp:send scope.
| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| to required | string |
| type | string Default: "template" Enum: "template" "text" |
object Required when type=template. | |
object Required when type=text. |
{- "to": "919800000000",
- "type": "template",
- "template": {
- "name": "invoice_due",
- "language": "en",
- "variables": [
- "Acme",
- "3540.00"
]
}, - "text": {
- "body": "Thanks for your order!"
}
}{- "data": {
- "id": "msg_7b3c",
- "channel": "sms",
- "status": "queued",
- "to": "string",
- "sent_at": "2019-08-24T14:15:22Z",
- "delivered_at": "2019-08-24T14:15:22Z",
- "read_at": "2019-08-24T14:15:22Z",
- "failed_at": "2019-08-24T14:15:22Z",
- "error": "string",
- "created_at": "2019-08-24T14:15:22Z"
}
}| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| id required | string The provider message id returned by a send call. |
{- "data": {
- "id": "msg_7b3c",
- "channel": "sms",
- "status": "queued",
- "to": "string",
- "sent_at": "2019-08-24T14:15:22Z",
- "delivered_at": "2019-08-24T14:15:22Z",
- "read_at": "2019-08-24T14:15:22Z",
- "failed_at": "2019-08-24T14:15:22Z",
- "error": "string",
- "created_at": "2019-08-24T14:15:22Z"
}
}Sends a transactional email through your company's configured email provider (Settings → Marketing → Email provider); if none is set the platform default mailer is used. The sender identity is taken from that provider — you cannot set an arbitrary from. HTML is sanitised server‑side (script/iframe/event‑handler vectors are stripped). Up to 50 recipients per call.
| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
required | Array of objects One recipient or an array. Each item may be a plain email string or an object. |
| subject required | string <= 255 characters |
| html required | string Email body as HTML. Sanitised server‑side before send. |
{- "to": [
- {
- "email": "user@example.com",
- "name": "string"
}
], - "subject": "string",
- "html": "string"
}{- "data": {
- "id": "msg_7b3c",
- "channel": "sms",
- "status": "queued",
- "to": "string",
- "sent_at": "2019-08-24T14:15:22Z",
- "delivered_at": "2019-08-24T14:15:22Z",
- "read_at": "2019-08-24T14:15:22Z",
- "failed_at": "2019-08-24T14:15:22Z",
- "error": "string",
- "created_at": "2019-08-24T14:15:22Z"
}
}| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
{- "data": [
- {
- "id": "string",
- "slug": "string",
- "title": "string",
- "department": "string",
- "location": "string",
- "employment_type": "string",
- "description": "string"
}
]
}| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| slug required | string |
{- "data": {
- "id": "string",
- "slug": "string",
- "title": "string",
- "department": "string",
- "location": "string",
- "employment_type": "string",
- "description": "string"
}
}| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| slug required | string |
| first_name required | string <= 100 characters |
| last_name | string <= 100 characters |
| email required | string <email> <= 255 characters |
| phone | string <= 30 characters |
| resume required | string <binary> Required. Allowed: pdf, doc, docx. Max 10 MB. |
| cover_letter | string <= 5000 characters |
| total_experience | number [ 0 .. 50 ] Years. |
| expected_salary | number |
| notice_period_days | integer >= 0 |
| linkedin_url | string <uri> <= 500 characters |
| skills | string A JSON-encoded string (not an array), e.g. '["PHP","Vue"]'. |
{- "success": false,
- "message": "Invalid API credentials, or the company was not found."
}| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
{- "success": false,
- "message": "Invalid API credentials, or the company was not found."
}A speculative application — the "send us your CV" form, for someone who is not applying to a specific opening. Everything else is the same as applying to a job; the application is simply filed with no job attached and surfaces in the same pipeline.
| first_name required | string <= 100 characters |
| last_name | string <= 100 characters |
| email required | string <email> |
| phone | string <= 30 characters |
| resume | string <binary> The CV. |
| cover_letter | string |
{- "success": true,
- "message": "string",
- "data": {
- "application_number": "string",
- "applicant_name": "string",
- "type": "general_application",
- "applied_at": "2019-08-24T14:15:22Z"
}
}Captures a lead. Send application/json, or multipart/form-data if you need to include an attachment file. Re-submitting an existing lead is de-duplicated server-side and still returns 200.
| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| first_name | string Required unless |
| last_name | string |
| full_name | string Used when |
string <email> Required unless | |
| phone | string Required unless |
| company_name | string |
| lead_type | string Enum: "contact" "rfp" "enquiry" "demo_request" |
| service | string |
| source | string |
| source_url | string <uri> |
| form_code | string |
| message | string <= 10000 characters |
| job_title | string |
| company_size | string |
| industry | string |
| country | string |
| services_needed | string |
| budget_range | string |
| timeline | string |
| project_overview | string |
| goals_expectations | string |
| current_systems | string |
| how_heard | string |
| meta_lead_id | string^\d+$ For a Facebook / Instagram lead-form lead: the lead id Meta issued (15-17 digits). Send it as a string, since a JSON number that long loses digits. With it, the stages of the lead can be reported back to Meta (Meta Conversion Leads). Sending a lead id that was already received returns |
| meta_form_id | string^\d+$ Meta lead form id. |
| meta_ad_id | string^\d+$ Meta ad id. |
| meta_adset_id | string^\d+$ Meta ad set id. |
| meta_campaign_id | string^\d+$ Meta campaign id. |
| meta_page_id | string^\d+$ Facebook Page id. |
| meta_created_time | string <date-time> When the person submitted the Meta lead form, as Meta reports it. |
| fbclid | string <= 500 characters The |
| fbc | string <= 255 characters A ready-made Meta |
{- "first_name": "string",
- "last_name": "string",
- "full_name": "string",
- "email": "user@example.com",
- "phone": "string",
- "company_name": "string",
- "lead_type": "contact",
- "service": "string",
- "source": "string",
- "form_code": "string",
- "message": "string",
- "job_title": "string",
- "company_size": "string",
- "industry": "string",
- "country": "string",
- "services_needed": "string",
- "budget_range": "string",
- "timeline": "string",
- "project_overview": "string",
- "goals_expectations": "string",
- "current_systems": "string",
- "how_heard": "string",
- "meta_lead_id": "string",
- "meta_form_id": "string",
- "meta_ad_id": "string",
- "meta_adset_id": "string",
- "meta_campaign_id": "string",
- "meta_page_id": "string",
- "meta_created_time": "2019-08-24T14:15:22Z",
- "fbclid": "string",
- "fbc": "string"
}{- "success": false,
- "message": "Invalid API credentials, or the company was not found."
}| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
{- "success": false,
- "message": "Invalid API credentials, or the company was not found."
}| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
{- "success": false,
- "message": "Invalid API credentials, or the company was not found."
}| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| productId required | string |
{- "success": false,
- "message": "Invalid API credentials, or the company was not found."
}Creates a sales order (document_type=sales_order, status draft) and starts a payment intent — it does not create an invoice, post a journal entry, or reserve or decrement stock. The order becomes a real invoice only after the payment gateway webhook confirms payment. A tenant customer is created (or matched by email, then by the last 9 digits of the phone) as a side effect.
Prices are server-authoritative when product_id is supplied: your unit_price and name are discarded and replaced with the catalogue values. They are only honoured for ad-hoc lines that omit product_id.
⚠️ This endpoint does not support idempotency — the Idempotency-Key header is ignored here, so a retry creates a duplicate order.
| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
required | Array of objects [ 1 .. 50 ] items |
required | object |
| currency required | string = 3 characters Enum: "INR" "USD" "EUR" "GBP" "AUD" "CAD" "SGD" "MYR" |
| source_website required | string <= 255 characters |
| return_url required | string <uri> <= 500 characters |
| cancel_url required | string <uri> <= 500 characters |
| tax_amount | number |
| shipping_amount | number |
| notes | string |
| metadata | object |
{- "items": [
- {
- "name": "string",
- "quantity": 0.001,
- "unit_price": 0.01,
- "product_id": 0,
- "description": "string",
- "hsn_sac_code": "string"
}
], - "customer": {
- "name": "string",
- "email": "user@example.com",
- "phone": "string",
- "address": "string",
- "city": "string",
- "state": "string",
- "pincode": "string",
- "country": "strin"
}, - "currency": "INR",
- "source_website": "string",
- "tax_amount": 0,
- "shipping_amount": 0,
- "notes": "string",
- "metadata": { }
}{- "success": true,
- "order_id": 0,
- "order_number": "string",
- "total_amount": 0,
- "currency": "string",
- "gateway": "razorpay",
- "payment_data": { }
}Returns an order created through this storefront checkout. Documents raised inside the app are not visible here and return 404, even though they live in the same company — this endpoint is deliberately scoped to storefront-origin orders only.
invoice_url stays null until the order converts to an invoice (i.e. until the payment webhook confirms payment).
| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| orderId required | string |
{- "success": false,
- "message": "Invalid API credentials, or the company was not found."
}✅ Available. Projects, sprints, and the work inside them — a user story, a task and a bug are one TYPED work item, so they are one endpoint with a type. Status changes have their own endpoint because they write the history the burndown and cycle time are derived from. Lists are cursor-paged, writes accept Idempotency-Key, and GET /assignees resolves a person to the id assigned_to wants. Iterations, areas and milestones have their own endpoints — iterations and areas are trees with a derived path.
The active employees assigned_to accepts, so you can resolve a person to an id instead of hardcoding one.
Four fields, deliberately. employees is an HR table — it carries salary, bank and identity columns — and this is a picker, not an HR export. Nothing beyond id, name, employee code and designation is returned, and there is no employee API behind it.
An assigned_to that is not in this list is rejected with 422; it is not silently ignored.
{- "success": true,
- "data": [
- {
- "id": 41,
- "name": "Priya Raman",
- "employee_code": "EMP-0041",
- "designation": "Senior Developer"
}
]
}The work-item types, priorities, the five fixed state categories, and this company's own configured statuses.
Call it once before you write anything. state is free text — a company renames "In Review" to whatever it likes — so a client that hardcodes a status list breaks on the first rename. The state values returned here are the ones this company actually uses, each with the state_category it belongs to.
{- "success": true,
- "data": {
- "work_item_types": [
- "epic"
], - "priorities": [
- "urgent"
], - "state_categories": [
- "proposed"
], - "project_statuses": [
- {
- "entity_type": "task",
- "name": "In Review",
- "slug": "review",
- "state": "in_review",
- "state_category": "in_progress",
- "is_closed": true
}
], - "sprint_statuses": [
- "planned"
]
}
}| status | string e.g. active |
| search | string Matches name or code. |
| limit | integer [ 1 .. 100 ] Default: 25 Page size (1–100). |
| cursor | string Opaque cursor from the previous response's paging.next_cursor. |
{- "success": true,
- "data": [
- {
- "id": 12,
- "name": "Website Rebuild",
- "code": "WEB",
- "description": "string",
- "status": "active",
- "start_date": "2019-08-24",
- "end_date": "2019-08-24",
- "is_billable": true,
- "created_at": "2019-08-24T14:15:22Z"
}
], - "paging": {
- "next_cursor": "string",
- "has_more": true
}
}code is optional. Left out, one is derived from the name — and the code becomes the prefix of every work-item reference in the project, so "Website Rebuild" yields WEB-1, WEB-2, … Send your own if the team already says "WEB-14" out loud. It must be unique within the company.
| Idempotency-Key | string <uuid> A unique UUID so retries don't double‑create/charge/send. Cached 24h. |
| name required | string <= 255 characters |
| code | string <= 50 characters Unique per company. Derived from the name when omitted. |
| description | string |
| status | string Default: "active" |
| start_date | string <date> |
| end_date | string <date> |
| is_billable | boolean |
{- "name": "Website Rebuild",
- "code": "WEB",
- "description": "string",
- "status": "active",
- "start_date": "2019-08-24",
- "end_date": "2019-08-24",
- "is_billable": true
}{- "success": true,
- "data": {
- "id": 12,
- "name": "Website Rebuild",
- "code": "WEB",
- "description": "string",
- "status": "active",
- "start_date": "2019-08-24",
- "end_date": "2019-08-24",
- "is_billable": true,
- "created_at": "2019-08-24T14:15:22Z"
}
}| project required | integer Project id. |
{- "success": true,
- "data": {
- "id": 12,
- "name": "Website Rebuild",
- "code": "WEB",
- "description": "string",
- "status": "active",
- "start_date": "2019-08-24",
- "end_date": "2019-08-24",
- "is_billable": true,
- "created_at": "2019-08-24T14:15:22Z"
}
}| project required | integer Project id. |
| Idempotency-Key | string <uuid> A unique UUID so retries don't double‑create/charge/send. Cached 24h. |
| name | string |
| code | string |
| description | string or null |
| status | string |
| start_date | string or null <date> |
| end_date | string or null <date> |
| is_billable | boolean |
{- "name": "string",
- "code": "string",
- "description": "string",
- "status": "string",
- "start_date": "2019-08-24",
- "end_date": "2019-08-24",
- "is_billable": true
}{- "success": true,
- "data": {
- "id": 12,
- "name": "Website Rebuild",
- "code": "WEB",
- "description": "string",
- "status": "active",
- "start_date": "2019-08-24",
- "end_date": "2019-08-24",
- "is_billable": true,
- "created_at": "2019-08-24T14:15:22Z"
}
}{- "success": true,
- "data": [
- {
- "id": 0,
- "project_id": 0,
- "name": "Sprint 12",
- "goal": "string",
- "status": "planned",
- "start_date": "2019-08-24",
- "end_date": "2019-08-24"
}
]
}| project required | integer |
| Idempotency-Key | string <uuid> A unique UUID so retries don't double‑create/charge/send. Cached 24h. |
| name required | string |
| goal | string |
| start_date | string <date> |
| end_date | string <date> |
| status | string Default: "planned" Enum: "planned" "active" "completed" |
{- "name": "Sprint 12",
- "goal": "Ship the checkout rewrite",
- "start_date": "2019-08-24",
- "end_date": "2019-08-24",
- "status": "planned"
}{- "success": true,
- "data": {
- "id": 0,
- "project_id": 0,
- "name": "Sprint 12",
- "goal": "string",
- "status": "planned",
- "start_date": "2019-08-24",
- "end_date": "2019-08-24"
}
}| project required | integer |
| sprint required | integer |
| Idempotency-Key | string <uuid> A unique UUID so retries don't double‑create/charge/send. Cached 24h. |
| name | string |
| goal | string or null |
| start_date | string or null <date> |
| end_date | string or null <date> |
| status | string Enum: "planned" "active" "completed" |
{- "name": "string",
- "goal": "string",
- "start_date": "2019-08-24",
- "end_date": "2019-08-24",
- "status": "planned"
}{- "success": true,
- "data": {
- "id": 0,
- "project_id": 0,
- "name": "Sprint 12",
- "goal": "string",
- "status": "planned",
- "start_date": "2019-08-24",
- "end_date": "2019-08-24"
}
}Iterations are the time boxes a board plans in, and they nest — a release containing sprints containing weeks.
Iterations are a TREE: send parent_id to nest one. path is derived from the ancestry ("Release 1/Sprint 2") and is read-only — sending it is not supported, because a path that disagrees with its parent_id is a node that displays in one place and belongs in another.
| project required | integer Project id. |
{- "success": true,
- "data": [
- {
- "id": 0,
- "project_id": 0,
- "parent_id": 0,
- "name": "Sprint 2",
- "path": "Release 1/Sprint 2",
- "sprint_id": 0,
- "start_date": "2019-08-24",
- "end_date": "2019-08-24",
- "sort_order": 0
}
]
}| project required | integer Project id. |
| Idempotency-Key | string <uuid> A unique UUID so retries don't double‑create/charge/send. Cached 24h. |
| name required | string <= 255 characters |
| parent_id | integer or null Another iteration in THIS project. |
| sprint_id | integer or null A sprint in THIS project. |
| start_date | string <date> |
| end_date | string <date> Must not precede start_date. |
| sort_order | integer |
{- "name": "Sprint 2",
- "parent_id": 0,
- "sprint_id": 0,
- "start_date": "2019-08-24",
- "end_date": "2019-08-24",
- "sort_order": 0
}{- "success": true,
- "data": {
- "id": 0,
- "project_id": 0,
- "parent_id": 0,
- "name": "Sprint 2",
- "path": "Release 1/Sprint 2",
- "sprint_id": 0,
- "start_date": "2019-08-24",
- "end_date": "2019-08-24",
- "sort_order": 0
}
}Reparenting is refused if it would make a loop — a node cannot become its own ancestor. Changing name or parent_id rebuilds path.
A body naming none of the fields below is refused with 422 rather than answered "updated", so a call that changes nothing cannot look like one that worked.
| project required | integer |
| iteration required | integer |
| Idempotency-Key | string <uuid> A unique UUID so retries don't double‑create/charge/send. Cached 24h. |
| name | string |
| parent_id | integer or null |
| sprint_id | integer or null |
| start_date | string or null <date> |
| end_date | string or null <date> |
| sort_order | integer |
{- "name": "string",
- "parent_id": 0,
- "sprint_id": 0,
- "start_date": "2019-08-24",
- "end_date": "2019-08-24",
- "sort_order": 0
}{- "success": true,
- "data": {
- "id": 0,
- "project_id": 0,
- "parent_id": 0,
- "name": "Sprint 2",
- "path": "Release 1/Sprint 2",
- "sprint_id": 0,
- "start_date": "2019-08-24",
- "end_date": "2019-08-24",
- "sort_order": 0
}
}Areas are the parts of the product work belongs to — "Checkout", "Checkout/Payments" — and they nest the same way iterations do. path is derived and read-only.
| project required | integer Project id. |
{- "success": true,
- "data": [
- {
- "id": 0,
- "project_id": 0,
- "parent_id": 0,
- "name": "Payments",
- "path": "Checkout/Payments",
- "sort_order": 0
}
]
}| project required | integer Project id. |
| Idempotency-Key | string <uuid> A unique UUID so retries don't double‑create/charge/send. Cached 24h. |
| name required | string <= 255 characters |
| parent_id | integer or null Another area in THIS project. |
| sort_order | integer |
{- "name": "Payments",
- "parent_id": 0,
- "sort_order": 0
}{- "success": true,
- "data": {
- "id": 0,
- "project_id": 0,
- "parent_id": 0,
- "name": "Payments",
- "path": "Checkout/Payments",
- "sort_order": 0
}
}Reparenting is refused if it would make a loop. A body naming none of the fields is refused with 422.
| project required | integer |
| area required | integer |
| Idempotency-Key | string <uuid> A unique UUID so retries don't double‑create/charge/send. Cached 24h. |
| name | string |
| parent_id | integer or null |
| sort_order | integer |
{- "name": "string",
- "parent_id": 0,
- "sort_order": 0
}{- "success": true,
- "data": {
- "id": 0,
- "project_id": 0,
- "parent_id": 0,
- "name": "Payments",
- "path": "Checkout/Payments",
- "sort_order": 0
}
}Dated commitments a project is measured against. Flat, not a tree.
| project required | integer Project id. |
{- "success": true,
- "data": [
- {
- "id": 0,
- "project_id": 0,
- "name": "Public beta",
- "description": "string",
- "status": "pending",
- "due_date": "2019-08-24",
- "completed_date": "2019-08-24",
- "sort_order": 0
}
]
}| project required | integer Project id. |
| Idempotency-Key | string <uuid> A unique UUID so retries don't double‑create/charge/send. Cached 24h. |
| name required | string <= 255 characters |
| description | string |
| due_date | string <date> |
| completed_date | string <date> |
| status | string Default: "pending" Enum: "pending" "in_progress" "completed" "overdue" |
| sort_order | integer |
{- "name": "Public beta",
- "description": "string",
- "due_date": "2019-08-24",
- "completed_date": "2019-08-24",
- "status": "pending",
- "sort_order": 0
}{- "success": true,
- "data": {
- "id": 0,
- "project_id": 0,
- "name": "Public beta",
- "description": "string",
- "status": "pending",
- "due_date": "2019-08-24",
- "completed_date": "2019-08-24",
- "sort_order": 0
}
}status and completed_date move together. Setting status: completed without a date stamps today; moving out of completed clears the date. Otherwise "delivered on" reads null for finished work, or carries a finish date for work that is not finished.
A body naming none of the fields is refused with 422.
| project required | integer |
| milestone required | integer |
| Idempotency-Key | string <uuid> A unique UUID so retries don't double‑create/charge/send. Cached 24h. |
| name | string |
| description | string or null |
| due_date | string or null <date> |
| completed_date | string or null <date> |
| status | string Enum: "pending" "in_progress" "completed" "overdue" |
| sort_order | integer |
{- "name": "string",
- "description": "string",
- "due_date": "2019-08-24",
- "completed_date": "2019-08-24",
- "status": "pending",
- "sort_order": 0
}{- "success": true,
- "data": {
- "id": 0,
- "project_id": 0,
- "name": "Public beta",
- "description": "string",
- "status": "pending",
- "due_date": "2019-08-24",
- "completed_date": "2019-08-24",
- "sort_order": 0
}
}User stories, tasks, bugs, epics — one list, filtered by type.
Use open_only=true for "not finished": it excludes both the completed and the removed categories, which is the thing most easily got wrong by listing categories by hand.
| project required | integer |
| type | string Enum: "epic" "feature" "user_story" "task" "bug" "issue" "test_case" |
| state | string |
| state_category | string Enum: "proposed" "in_progress" "resolved" "completed" "removed" |
| priority | string Enum: "urgent" "high" "medium" "low" |
| sprint_id | integer |
| parent_id | integer |
| assigned_to | integer |
| iteration_id | integer |
| area_id | integer |
| external_id | string Your own id for one item. |
| external_ids | string Example: external_ids=FP-S10-08-T02,FP-S10-08-T03 Comma-separated, up to 100. Returns only the ones that already exist, so an importer can ask "which of these do I already have" in one call instead of reading the whole backlog to find out. Every match comes back in one response. When |
| open_only | boolean Excludes completed AND removed. |
| search | string Matches title or reference. |
| limit | integer [ 1 .. 100 ] Default: 25 Page size (1–100). |
| cursor | string Opaque cursor from the previous response's paging.next_cursor. |
{- "success": true,
- "data": [
- {
- "id": 0,
- "external_id": "string",
- "reference": "WEB-14",
- "project_id": 0,
- "type": "epic",
- "title": "string",
- "description": "string",
- "state": "in_review",
- "state_category": "proposed",
- "priority": "urgent",
- "severity": "string",
- "parent_id": 0,
- "sprint_id": 0,
- "iteration_id": 0,
- "assigned_to": 0,
- "story_points": 0,
- "estimated_hours": 0,
- "remaining_hours": 0,
- "completed_hours": 0,
- "start_date": "2019-08-24",
- "due_date": "2019-08-24",
- "completed_at": "2019-08-24T14:15:22Z",
- "acceptance_criteria": "string",
- "as_a": "string",
- "i_want": "string",
- "so_that": "string",
- "repro_steps": "string",
- "tags": [
- "string"
], - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "status_history": [
- {
- "from_state": "string",
- "to_state": "string",
- "from_category": "string",
- "to_category": "string",
- "changed_at": "2019-08-24T14:15:22Z",
- "seconds_in_previous_state": 0
}
]
}
], - "paging": {
- "next_cursor": "string",
- "has_more": true
}
}One endpoint for all of them — type decides which. The fields that only make sense for one type are simply optional: as_a / i_want / so_that and acceptance_criteria for a user story, repro_steps and severity for a bug.
The reference is generated, per project, under a lock — WEB-1, WEB-2 — so two concurrent callers can never be handed the same one. Do not send it.
State. Send nothing and the item opens at new / proposed. Otherwise send either a status slug from /meta (translated for you), or a state with its state_category. A state this company has not configured is rejected rather than filed under a guessed category — guessing is how a board starts claiming work is finished when it is not.
| project required | integer |
| Idempotency-Key | string <uuid> A unique UUID so retries don't double‑create/charge/send. Cached 24h. |
| type required | string Enum: "epic" "feature" "user_story" "task" "bug" "issue" "test_case" |
| title required | string <= 500 characters |
| description | string |
| status | string A slug from /meta, e.g. review. Translated to state + category. |
| state | string Send state_category with it unless the company has configured this state. |
| state_category | string Enum: "proposed" "in_progress" "resolved" "completed" "removed" |
| priority | string Enum: "urgent" "high" "medium" "low" |
| severity | string Bugs. |
| parent_id | integer Must be in the SAME project. |
| sprint_id | integer Must be in the SAME project. |
| iteration_id | integer |
| assigned_to | integer Employee id. |
| story_points | integer |
| estimated_hours | number |
| start_date | string <date> |
| due_date | string <date> |
| acceptance_criteria | string User stories. |
| as_a | string User stories. |
| i_want | string User stories. |
| so_that | string User stories. |
| repro_steps | string Bugs. |
| external_id | string <= 255 characters YOUR id for this item. Unique among live items in the project; reusing one returns 422 naming the item that holds it, so you can PATCH that instead of creating a second. Retired (removed) items release theirs. |
| tags | Array of strings |
{- "type": "epic",
- "title": "string",
- "description": "string",
- "status": "string",
- "state": "string",
- "state_category": "proposed",
- "priority": "urgent",
- "severity": "string",
- "parent_id": 0,
- "sprint_id": 0,
- "iteration_id": 0,
- "assigned_to": 0,
- "story_points": 0,
- "estimated_hours": 0,
- "start_date": "2019-08-24",
- "due_date": "2019-08-24",
- "acceptance_criteria": "string",
- "as_a": "string",
- "i_want": "string",
- "so_that": "string",
- "repro_steps": "string",
- "external_id": "string",
- "tags": [
- "string"
]
}{- "success": true,
- "data": {
- "id": 0,
- "external_id": "string",
- "reference": "WEB-14",
- "project_id": 0,
- "type": "epic",
- "title": "string",
- "description": "string",
- "state": "in_review",
- "state_category": "proposed",
- "priority": "urgent",
- "severity": "string",
- "parent_id": 0,
- "sprint_id": 0,
- "iteration_id": 0,
- "assigned_to": 0,
- "story_points": 0,
- "estimated_hours": 0,
- "remaining_hours": 0,
- "completed_hours": 0,
- "start_date": "2019-08-24",
- "due_date": "2019-08-24",
- "completed_at": "2019-08-24T14:15:22Z",
- "acceptance_criteria": "string",
- "as_a": "string",
- "i_want": "string",
- "so_that": "string",
- "repro_steps": "string",
- "tags": [
- "string"
], - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "status_history": [
- {
- "from_state": "string",
- "to_state": "string",
- "from_category": "string",
- "to_category": "string",
- "changed_at": "2019-08-24T14:15:22Z",
- "seconds_in_previous_state": 0
}
]
}
}Includes status_history — every state change with how long it sat in the previous one.
| project required | integer |
| workItem required | integer |
{- "success": true,
- "data": {
- "id": 0,
- "external_id": "string",
- "reference": "WEB-14",
- "project_id": 0,
- "type": "epic",
- "title": "string",
- "description": "string",
- "state": "in_review",
- "state_category": "proposed",
- "priority": "urgent",
- "severity": "string",
- "parent_id": 0,
- "sprint_id": 0,
- "iteration_id": 0,
- "assigned_to": 0,
- "story_points": 0,
- "estimated_hours": 0,
- "remaining_hours": 0,
- "completed_hours": 0,
- "start_date": "2019-08-24",
- "due_date": "2019-08-24",
- "completed_at": "2019-08-24T14:15:22Z",
- "acceptance_criteria": "string",
- "as_a": "string",
- "i_want": "string",
- "so_that": "string",
- "repro_steps": "string",
- "tags": [
- "string"
], - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "status_history": [
- {
- "from_state": "string",
- "to_state": "string",
- "from_category": "string",
- "to_category": "string",
- "changed_at": "2019-08-24T14:15:22Z",
- "seconds_in_previous_state": 0
}
]
}
}Everything except the state. Status changes go to /transition — a state change writes the history row the burndown, the cumulative-flow diagram and cycle time are derived from, so it is an event rather than a field, and you should not be able to move a card by accident while renaming its title.
| project required | integer |
| workItem required | integer |
| Idempotency-Key | string <uuid> A unique UUID so retries don't double‑create/charge/send. Cached 24h. |
| title | string |
| description | string or null |
| type | string Enum: "epic" "feature" "user_story" "task" "bug" "issue" "test_case" |
| priority | string Enum: "urgent" "high" "medium" "low" |
| severity | string or null |
| parent_id | integer or null |
| sprint_id | integer or null |
| assigned_to | integer or null |
| story_points | integer or null |
| estimated_hours | number or null |
| remaining_hours | number or null |
| completed_hours | number or null |
| start_date | string or null <date> |
| due_date | string or null <date> |
| acceptance_criteria | string or null |
| repro_steps | string or null |
| external_id | string <= 255 characters YOUR id for this item. Unique among live items in the project; reusing one returns 422 naming the item that holds it, so you can PATCH that instead of creating a second. Retired (removed) items release theirs. |
| tags | Array of strings or null |
{- "title": "string",
- "description": "string",
- "type": "epic",
- "priority": "urgent",
- "severity": "string",
- "parent_id": 0,
- "sprint_id": 0,
- "assigned_to": 0,
- "story_points": 0,
- "estimated_hours": 0,
- "remaining_hours": 0,
- "completed_hours": 0,
- "start_date": "2019-08-24",
- "due_date": "2019-08-24",
- "acceptance_criteria": "string",
- "repro_steps": "string",
- "external_id": "string",
- "tags": [
- "string"
]
}{- "success": true,
- "data": {
- "id": 0,
- "external_id": "string",
- "reference": "WEB-14",
- "project_id": 0,
- "type": "epic",
- "title": "string",
- "description": "string",
- "state": "in_review",
- "state_category": "proposed",
- "priority": "urgent",
- "severity": "string",
- "parent_id": 0,
- "sprint_id": 0,
- "iteration_id": 0,
- "assigned_to": 0,
- "story_points": 0,
- "estimated_hours": 0,
- "remaining_hours": 0,
- "completed_hours": 0,
- "start_date": "2019-08-24",
- "due_date": "2019-08-24",
- "completed_at": "2019-08-24T14:15:22Z",
- "acceptance_criteria": "string",
- "as_a": "string",
- "i_want": "string",
- "so_that": "string",
- "repro_steps": "string",
- "tags": [
- "string"
], - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "status_history": [
- {
- "from_state": "string",
- "to_state": "string",
- "from_category": "string",
- "to_category": "string",
- "changed_at": "2019-08-24T14:15:22Z",
- "seconds_in_previous_state": 0
}
]
}
}The status update.
Send a status slug from /meta, or a state with its state_category. Entering the completed category stamps the finish time; leaving it clears it again, so "finished on" cannot outlive a reopen.
An optional comment is recorded on the item's timeline — without it the board says what changed and never why.
| project required | integer |
| workItem required | integer |
| Idempotency-Key | string <uuid> A unique UUID so retries don't double‑create/charge/send. Cached 24h. |
| status | string A slug from /meta. Send this OR state. |
| state | string |
| state_category | string Enum: "proposed" "in_progress" "resolved" "completed" "removed" |
| comment | string |
{- "status": "done",
- "state": "in_review",
- "state_category": "proposed",
- "comment": "Deployed to staging"
}{- "success": true,
- "message": "WEB-14 moved from active to done.",
- "data": {
- "id": 0,
- "external_id": "string",
- "reference": "WEB-14",
- "project_id": 0,
- "type": "epic",
- "title": "string",
- "description": "string",
- "state": "in_review",
- "state_category": "proposed",
- "priority": "urgent",
- "severity": "string",
- "parent_id": 0,
- "sprint_id": 0,
- "iteration_id": 0,
- "assigned_to": 0,
- "story_points": 0,
- "estimated_hours": 0,
- "remaining_hours": 0,
- "completed_hours": 0,
- "start_date": "2019-08-24",
- "due_date": "2019-08-24",
- "completed_at": "2019-08-24T14:15:22Z",
- "acceptance_criteria": "string",
- "as_a": "string",
- "i_want": "string",
- "so_that": "string",
- "repro_steps": "string",
- "tags": [
- "string"
], - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "status_history": [
- {
- "from_state": "string",
- "to_state": "string",
- "from_category": "string",
- "to_category": "string",
- "changed_at": "2019-08-24T14:15:22Z",
- "seconds_in_previous_state": 0
}
]
}
}Public comments only — the team's internal notes are not returned to an API key.
| project required | integer |
| workItem required | integer |
{- "success": true,
- "data": [
- {
- "id": 0,
- "body": "string",
- "author": "string",
- "created_at": "2019-08-24T14:15:22Z"
}
]
}author is a display name only. An API key is not a user, so no team member's name is attached to it — left out, the comment is signed with the key's own name.
| project required | integer |
| workItem required | integer |
| Idempotency-Key | string <uuid> A unique UUID so retries don't double‑create/charge/send. Cached 24h. |
| body required | string |
| author | string |
{- "body": "string",
- "author": "Jenkins"
}{- "success": true,
- "data": {
- "id": 0,
- "body": "string",
- "author": "string",
- "created_at": "2019-08-24T14:15:22Z"
}
}✅ Available. Driving-school booking flow — service locations, instructors by suburb, lessons & packages, availability, and bookings with online payment. Accepts a secret key OR a browser publishable (pk_) key.
Active service locations (test centres / pickup points / offices).
| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
{- "success": true,
- "locations": [
- {
- "id": 0,
- "name": "string",
- "type": "test_center",
- "city": "string",
- "state": "string",
- "postcode": "string",
- "latitude": 0,
- "longitude": 0
}
]
}Bookable instructors, optionally filtered to those who service a suburb/postcode/point.
| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| lat | number Latitude (with lng) — distance match vs each instructor's radius_km. |
| lng | number Longitude (with lat). |
| postcode | string Instructors servicing this postcode. |
| suburb | string Instructors servicing this suburb. |
| location_id | integer Instructors mapped to this service location. |
| transmission | string Enum: "auto" "manual" "both" Instructor's transmission (a custom field — |
| gender | string Case-insensitive exact match. |
| language | string Instructor speaks this language. |
| nationality | string Case-insensitive exact match. |
| car | string Filter by the instructor's car. |
| available_date | string <date> Only instructors with free capacity on this date. |
| time_of_day | string Enum: "morning" "afternoon" "evening" Narrows |
| with_next | boolean Include each instructor's |
| cf | object Match arbitrary instructor custom fields, e.g. |
{- "success": true,
- "instructors": [
- {
- "id": 0,
- "name": "string",
- "gender": "string",
- "languages": [
- "string"
], - "transmission": "string",
- "bio": "string",
- "photo_url": "string",
- "service_areas": [
- {
- "suburb": "string",
- "postcode": "string",
- "state": "string",
- "radius_km": 0
}
]
}
]
}| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| transmission | string Enum: "auto" "manual" |
| type | string Enum: "lesson" "package" |
| instructor_id | integer Only the lessons/packages this instructor actually offers. |
{- "success": true,
- "courses": [
- {
- "id": 0,
- "name": "string",
- "price": 0,
- "currency": "AUD",
- "duration_minutes": 0,
- "transmission": "string",
- "is_package": true
}
]
}Bookable slots for an instructor on a date, honouring the weekly schedule, overrides, existing bookings and booking settings.
| company required | string Example: acme-traders Your company (workspace) slug — the same value as your Copy it from Settings → API Keys → “Integration details”, which shows the slug and the ready-made endpoint URL with a copy button. It is also in Settings → Company Profile. ⚠️ Do not derive it from the company name. It is not your display name, brand or product name, and it is not reliably a slugified version of them either — workspaces are often created under one name, or renamed later, while the slug stays as first issued. A workspace branded “Acme Halls” can legitimately have a slug that looks nothing like it. Guessing produces a plausible-looking URL that always fails. Copy it, don't infer it. An unknown slug returns |
| instructor_id required | integer |
| date required | string <date> |
| duration_minutes | integer [ 15 .. 480 ] Slot length to fit. Outside 15–480 returns 422. |