TicketStacker

Help › API and integrations

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/json with 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: 41250 is $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 options exactly; 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

NextConnect your own AI

Still stuck? Email hello@ticketstacker.com. A real person reads every message.