Create a ticket from another system

A complete curl saq for creating a ticket from a monitoring tool, a form, or another ticket system, including Inbox routing, assignees, reading it back, commenting, and attaching a file.

Written for
integration-developer
Roles
member, admin, owner, agent
Requires
API token with the write scope (triage to create in the Inbox)
Feature
tickets

This recipe creates a ticket in SAQ from another system with plain HTTP calls. It uses the workspace acme, the project key website, and a personal or agent token with the write scope in the Authorization header of every request.

1. Choose the project

List the projects the token can see and pick one. You need the project's id for the create call:

curl "https://saq.no/api/w/acme/projects" \
  -H "Authorization: Bearer saq_YOUR_TOKEN"

Each item has id, key, name, billingClient, billingMode, and archived. If you want a specific kind, state, priority, or label, fetch their ids from GET /api/w/acme/workflow (kinds, states, priorities, labels, each with id, key, name).

2. Create the ticket

curl -X POST "https://saq.no/api/w/acme/tickets" \
  -H "Authorization: Bearer saq_YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "01924c3e-0000-7000-8000-000000000004",
    "title": "Checkout returns 500 after payment",
    "description": "Alert PAY-2291 from the monitoring system.\n\nFirst seen 2026-09-19 10:12 UTC.",
    "kindId": "01924c3e-0000-7000-8000-000000000001",
    "priorityId": "01924c3e-0000-7000-8000-000000000003",
    "dueDate": "2026-09-22",
    "labelIds": []
  }'

Only projectId and title (1–200 characters) are required. description is Markdown up to 50 000 characters. Optional fields: kindId, stateId, priorityId, assigneeId, billingClientId, labelIds (≤20), startDate, dueDate, estimateMinutes, parentId. Omitted values take the workspace defaults: the reopen-target state, the workspace's default priority, and the project's billing client.

The response is 201 with the ticket summary; keep id and humanId (for example ACME-185) in your system.

State, priority, dates, and labels are triage fields: they are applied when the token's owner is an Owner or Admin or a full member of the project, and silently ignored otherwise.

Creating in the Inbox instead

Send "projectId": null to create the ticket in the shared Inbox for a person to triage. This needs the triage scope and a token without a project restriction; the ticket gets the workspace's inbound kind, has no billing client until it is routed, and cannot receive time until then.

Setting the assignee

assigneeId must be a person UUID; there is no lookup by email over HTTP, and GET /people is not open to tokens. Three ways to get the id:

  • Read GET /api/w/acme/projects/website: its members list carries identityId, name, and email for every project member.
  • Reuse assignee.id or reporter.id from tickets you already read.
  • Use the MCP tool create_ticket or update_ticket, whose assignee argument accepts an email address, me, or none.

Assignees are workspace users (or elevated client users with a full login), and the token's owner must be allowed to change the assignee (an Owner or Admin, or a full member of the project).

3. Read it back

curl "https://saq.no/api/w/acme/tickets/ACME-185" \
  -H "Authorization: Bearer saq_YOUR_TOKEN"

The ticket page adds description, viewers, comments, events, attachments, links, github, time (workspace users only), and permissions, which tells you what the token may do next (edit, comment audiences, moveState, logTime, and so on).

4. Add a shared comment

curl -X POST "https://saq.no/api/w/acme/tickets/ACME-185/comments" \
  -H "Authorization: Bearer saq_YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "body": "The alert cleared at 10:40 UTC. Root cause still open.",
    "audience": "shared"
  }'

audience is shared (everyone who sees the ticket, including client users), internal (workspace users and elevated client users), or staff (workspace users only). Omit it to use the ticket's default audience. mentions (≤20 person UUIDs) become viewer grants. The response (201) carries id, audience, body, bodyHtml, createdAt, and mentioned. Comments cannot be edited or deleted afterwards.

5. Attach a file

Upload as multipart/form-data with a file part and an optional audience part:

curl -X POST "https://saq.no/api/w/acme/tickets/ACME-185/attachments" \
  -H "Authorization: Bearer saq_YOUR_TOKEN" \
  -F "file=@screenshot.png" \
  -F "audience=shared"

The response (201) is the attachment (id, filename, contentType, size, inline, audience). Files are limited to 20 MiB and 30 uploads per minute; executables, scripts, and files whose bytes do not match the declared type are refused with 400 invalid and a reason. To attach the file to a comment instead of the ticket, upload first and pass its id in the comment's attachmentIds.

Idempotency

There is no idempotency key. Before creating, search for your own reference so a retried alert does not become a second ticket:

curl "https://saq.no/api/w/acme/tickets?q=PAY-2291&limit=5" \
  -H "Authorization: Bearer saq_YOUR_TOKEN"

If items contains a match, comment on it instead of creating. Store the returned humanId the moment a create succeeds, and never resend a request whose response you received. Tickets created this way show source: "api".