API reference
Create, update, move and close tickets from your own code or other tools, with a simple JSON API and a token.
TicketStacker has a REST API, so your website, booking tool, accounting system or a quick script can create and update tickets. It's included on every plan once your company is live. While you're setting up you can already make a token and check it with GET /me; everything else answers once you go live (see Setting up vs live).
Base address: https://app.ticketstacker.com/api/v1
Get a token
A company Admin opens Settings → API & webhooks, clicks Create token, gives it a name (like Website) and copies it. Copy it right away: you won't be able to see it again. Make a separate token for each tool, so you can revoke one without breaking the others.
Send the token in the Authorization header on every request:
curl https://app.ticketstacker.com/api/v1/stacks \
-H "Authorization: Bearer ts_your_token"
A token only ever sees its own company's stacks and tickets.
The basics
- Requests and responses are JSON. Send
Content-Type: application/jsonwith a body. - Rate limit: 120 requests a minute per token. Past that you get
429; wait a moment and retry. - Dates are
YYYY-MM-DD. Times are ISO 8601 in UTC. - Currency fields are in cents:
41250is $412.50. - Fields use keys, not labels. Get each stack's field keys, status keys and step keys from
GET /stacks. Keys never change, even when someone renames a field. - Choice fields take one of the field's
optionsexactly; multi-select fields take a list of them.
Endpoints
| Method | Path | What it does |
|---|---|---|
| GET | /me |
Check a token: your company, and whether it's live (works while setting up) |
| GET | /stacks |
Your stacks, with their fields, statuses and steps |
| GET | /stacks/{stackId}/tickets |
List tickets, newest first |
| POST | /stacks/{stackId}/tickets |
Create a ticket |
| GET | /tickets/{ticketId} |
Get one ticket |
| PATCH | /tickets/{ticketId} |
Update a ticket |
| POST | /tickets/{ticketId}/move |
Move a ticket to a step or status |
| POST | /tickets/{ticketId}/move-to-stack |
Move a ticket to another stack |
| POST | /stacks/{stackId}/clone |
Copy a stack's design into a new stack |
| POST | /tickets/{ticketId}/close |
Close a ticket |
| POST | /tickets/{ticketId}/comments |
Add a comment |
| GET | /tickets/{ticketId}/comments |
List a ticket's comments, oldest first |
| GET | /inventory/products |
Search inventory products (Inventory add-on) |
| GET | /inventory/stock |
Stock in warehouses and machines (Inventory add-on) |
| GET | /inventory/machines |
Machines with their status (Inventory add-on) |
| GET | /inventory/sales |
Machine sales for a date range, grouped (Inventory add-on) |
List stacks
GET /stacks returns { "stacks": [...] }:
{
"stacks": [{
"id": "3995074d-…",
"name": "Facility Requests",
"prefix": "FR",
"fields": [
{ "key": "location", "label": "Location", "type": "location", "required": true, "options": null },
{ "key": "category", "label": "Category", "type": "dropdown", "required": false, "options": ["Plumbing", "Electrical"] },
{ "key": "cost", "label": "Cost", "type": "currency", "required": false, "options": null }
],
"statuses": [
{ "key": "new", "name": "New", "closed": false, "steps": [{ "key": "triage", "name": "Triage" }] },
{ "key": "done", "name": "Done", "closed": true, "steps": [{ "key": "fixed", "name": "Fixed" }] }
]
}]
}
Create a ticket
POST /stacks/{stackId}/tickets
| Field | ||
|---|---|---|
title |
required | Longer titles are cut to 200 characters |
fields |
optional | { "field_key": value }. Required fields must be included |
due_date |
optional | YYYY-MM-DD |
owner_email |
optional | Must be someone in your company |
requester |
optional | { "name", "email", "company" }: the person asking |
curl -X POST https://app.ticketstacker.com/api/v1/stacks/{stackId}/tickets \
-H "Authorization: Bearer ts_your_token" \
-H "Content-Type: application/json" \
-d '{
"title": "Leaking faucet",
"fields": { "location": "South Lamar", "category": "Plumbing" },
"due_date": "2026-11-01",
"requester": { "name": "Ava Chen", "email": "ava@example.com" }
}'
It returns 201 and the new ticket. The ticket starts in the stack's first step, and that step's automatic actions and emails run just as if someone had made it in the app.
The ticket object
Every ticket endpoint returns { "ticket": {...} }:
{
"ticket": {
"id": "8c1e…",
"key": "FR-42",
"number": 42,
"title": "Leaking faucet",
"stack": { "id": "3995074d-…", "name": "Facility Requests" },
"status": { "key": "new", "name": "New" },
"step": { "key": "triage", "name": "Triage" },
"is_closed": false,
"fields": { "location": "South Lamar", "category": "Plumbing" },
"tags": ["urgent"],
"due_date": "2026-11-01",
"owner": { "name": "Jordan Lee", "email": "jordan@example.com" },
"requester": { "name": "Ava Chen", "email": "ava@example.com", "company": null },
"source": "api",
"url": "https://app.ticketstacker.com/stacks/3995074d-…/t/42",
"created_at": "2026-10-14T15:02:11Z",
"updated_at": "2026-10-14T15:02:11Z"
}
}
source tells you how the ticket came in: app, web_form, email, schedule, import or api.
List tickets
GET /stacks/{stackId}/tickets returns { "total": 128, "tickets": [...] }, newest first. Filters:
| Query | |
|---|---|
status |
A status key, like new |
open=true |
Only tickets that aren't closed |
updated_since |
ISO date or time. Great for syncing: ask for what changed since your last run |
limit, offset |
Paging. limit is 50 by default, up to 200 |
Update a ticket
PATCH /tickets/{ticketId} with any of title, fields, due_date and owner_email. fields are merged in, so send only the ones that change. Send null to clear a value.
curl -X PATCH https://app.ticketstacker.com/api/v1/tickets/{ticketId} \
-H "Authorization: Bearer ts_your_token" -H "Content-Type: application/json" \
-d '{ "fields": { "cost": 41250 }, "owner_email": "jordan@example.com" }'
Move a ticket
POST /tickets/{ticketId}/move with { "step": "step_key" } or { "status": "status_key" } (the status's first step). The current step's requirements must be met, just like in the app; if they aren't, you get 400 and a message saying what's missing.
Move a ticket to another stack
POST /tickets/{ticketId}/move-to-stack with { "stack_id": "…", "step": "step_key" } (step is optional; it starts at the stack's first step). The ticket keeps its comments, files and history and gets a number in the new stack; fields with the same key or label carry over, and the rest are written into the new stack's first long-text field. You get the ticket back. See Move a ticket to another stack.
Clone a stack
POST /stacks/{stackId}/clone with { "name": "Repairs (copy)", "prefix": "RPC" } (2 to 5 letters) makes a new stack in your company with the same design and settings, and no tickets. You get 201 with { "stack": { "id", "name", "prefix", "published" } }. See Clone a stack.
Close a ticket
POST /tickets/{ticketId}/close moves it to the stack's first closed status. Rules on closing still apply.
List comments
GET /tickets/{ticketId}/comments returns { "comments": [{ "id", "author", "visibility", "body", "created_at" }] }, oldest first.
Add a comment
POST /tickets/{ticketId}/comments with { "body": "…", "visibility": "internal" }. Use "public" to share it with the requester. Returns 201 and { "comment": { "id": "…" } }.
Inventory (read only)
These four need the Inventory add-on (otherwise 403). They only read. Money here is in dollars, not cents, because it comes from your purchase orders and sales reports.
Search products
GET /inventory/products?search=water&limit=50&offset=0 returns { "products": [{ "id", "upc", "name", "size", "vendor", "price", "unit_cost", "units_per_box", "box_price", "image_url", "warehouse_on_hand", "in_machines" }] }. search matches the name, the vendor, or the exact barcode; leave it out to list everything (50 at a time, up to 200).
Stock
GET /inventory/stock returns { "warehouses": [{ "warehouse_id", "warehouse", "product_id", "upc", "name", "on_hand", "reorder_point", "reorder_level", "low" }], "machines": [{ "machine_id", "machine", "product_id", "upc", "name", "current", "max", "to_replenish", "updated_at" }] }. Narrow it with product_id, warehouse_id or machine_id, and add low=true for only what's at or below its reorder point (warehouses) or needs restocking (machines).
Machines
GET /inventory/machines returns { "machines": [{ "id", "name", "nickname", "kind", "serial", "route", "address", "location", "stock": { "products", "items", "capacity", "fill_percent", "to_replenish", "updated_at" }, "last_sale_at", "sales_last_7_days" }] }. kind is vending or atm; location is the Location the machine is linked to on Places.
Sales summary
GET /inventory/sales?from=2026-11-01&to=2026-11-07&group_by=machine returns the range's sales, paid, pre_tax, items, orders, by_source (sales per system: sandstar, nayax, cantaloupe) and rows, one per machine, location, day or product (group_by), each with orders, qty, sales and paid. Dates are in your company's time zone (returned as time_zone); a year at most per request.
Errors
Errors come back as { "error": { "code": "…", "message": "…" } }. The message is written for people, so it's fine to show.
| Status | Code | Means |
|---|---|---|
| 400 | invalid |
Something in the request is wrong: a missing required field, an unknown option, a step whose requirements aren't met |
| 401 | unauthorized |
Missing, wrong or revoked token |
| 403 | forbidden |
Your company can't do that yet, for example before it's live |
| 403 | forbidden |
Also: the Inventory add-on isn't on, for the inventory endpoints |
| 404 | not_found |
No such stack or ticket in your company |
| 429 | rate_limited |
Over 120 requests a minute; slow down |
| 500 | server_error |
Our fault. Try again, and tell us if it keeps happening |
Going the other way
To have TicketStacker tell your system when something happens, add a webhook to a step. See Webhooks. To connect without writing code, see Connect TicketStacker to other apps.
Questions or something missing? Email hello@ticketstacker.com. We'd love to hear what you're building.
Related
- WebhooksHave TicketStacker call your system the moment a ticket reaches a step, with a signed JSON payload you can trust.
- Connect TicketStacker to other appsUse Zapier, Make or n8n to link TicketStacker with Slack, Google Sheets, QuickBooks and thousands of other apps, without writing code.
- API and webhooksCreate and update tickets from other tools, and let other tools know when a ticket reaches a step.
- Ticket fieldsEvery field type, when to use each, and how Required, help text and requester visibility make better tickets.
Still stuck? Email hello@ticketstacker.com. A real person reads every message.
