Appearance
5. Support
Open and work support tickets with Axiom, attach files, and host the support widget in your own product.
Read first: Support tickets.
Base URL https://api.axiomscript.com. Every request is signed; see Authentication and signing.
POST
/ticketsOpen a support ticketGET/ticketsList your ticketsGET/tickets/{ticketId}Read a ticket and its threadPOST/tickets/{ticketId}/messagesReply on a ticketPOST/tickets/{ticketId}/resolveResolve a ticketPOST/uploadsUpload a file, get a one-time attachment tokenGET/attachments/{attachmentId}Download a ticket attachmentPOST/support-sessionsMint a session for the embedded support widgetGET/support-sessions/domainsWhich domains may embed your support widget, and check oneOpen a support ticket
POST/api/inbound/{partnerSlug}/tickets
Opens a ticket addressed to Axiom. Optionally anchored to one of your orders by YOUR order reference, which is how a shipping or script problem gets tied to the script it is about. The ticket is created in the environment of the key you signed with.
Parameters
partnerSlugstringpathrequiredThe partner's Axiom-issued slug (e.g. acme-health).
Idempotency-KeystringheaderrequiredREQUIRED on every endpoint that creates or changes something (see the Idempotency section of the API description for the list). Omitting it is a 400 idempotency_key_required. Replaying the same key with the SAME body returns the original response. Reusing a key with a DIFFERENT body is a 409 idempotency_key_reused. Max 128 characters.
Request body
subjectstringrequired3 to 200 characters.
descriptionstringrequiredWhat is wrong, in your own words. This is the first message in the thread and we reply to it directly. 1 to 10000 characters.
categorystringoptionalWhat kind of problem this is. There is no technical or billing category yet; use other for an integration or invoice question and say so in the description. One of: address_change, prescriber_change, script_change, shipping_issue, patient_inquiry, other. Default "other".
prioritystringoptionalP1 is not available over the API: it pages our on-call. If something is genuinely P1, open the ticket at P2 and say so, and we will raise it. One of: P2, P3. Default "P3".
order_idstringoptionalYOUR order reference (the partner_order_id you sent on create), not our id. Optional, but include it whenever the ticket is about a specific script: without it we have to ask. Up to 128 characters.
attachmentsarray of objectoptionalUpload tokens from POST /uploads, each used once. At most 4 items.
attachments[].tokenstringrequiredThe upload.token from POST /uploads. Valid for 24 hours, usable once. 1 to 200 characters.
attachments[].file_namestringoptionalThe name to show for the file. Sent here, in the body, never in a URL or header. 1 to 200 characters.
Responses
| Status | Meaning |
|---|---|
201 | Ticket opened (or idempotent replay) |
400 | The request was malformed. validation_failed means the body or query failed schema validation (see issues). On a mutating endpoint you can also get idempotency_key_required (no Idempotency-Key header), idempotency_key_invalid (empty, or longer than 128 characters), or idempotency_key_mismatch (the header and the body's idempotency_key are different strings). |
401 | Missing, expired, or invalid HMAC signature / timestamp |
404 | not_found: ticketing is not enabled for your account. An order_id we cannot find does not fail the request (see order_id_unresolved on the 201). |
409 | idempotency_key_reused (this key was already used with a different body) or idempotency_key_in_flight (an identical request with this key is still being processed, so retry). Neither has written anything you do not already have. |
423 | partner_inactive or tenant_suspended. Your partner account is deactivated or suspended, so no request will be served until it is reinstated. |
429 | Per-partner rate limit exceeded |
Response fields (201)
oktruerequired
order_id_unresolvedtrueoptionalPresent only when order_id named no order of yours in this environment. The ticket is still opened, without an order link.
ticketobjectrequired
ticket.idintegerrequired
ticket.subjectstringrequired
ticket.categorystringrequired
ticket.prioritystringrequiredOne of: P1, P2, P3.
ticket.statusstringrequiredOne of: open, in_progress, resolved.
ticket.order_idstring | nullrequiredYour own order reference, or null when the ticket names no order.
ticket.created_atstring (date-time)required
ticket.updated_atstring (date-time)required
ticket.resolved_atstring (date-time) | nulloptional
Request
http
POST /api/inbound/acme-health/tickets
Host: api.axiomscript.com
Content-Type: application/json
X-Axiom-Timestamp: 1767225600
X-Axiom-Signature: sha256=<hex>
Idempotency-Key: <your key>json
{
"subject": "Package arrived damaged",
"description": "The clinic reports the outer box was crushed.",
"category": "shipping_issue",
"order_id": "ACME-0001"
}Response 201
json
{
"ok": true,
"ticket": {
"id": 7798,
"subject": "Package arrived damaged",
"category": "shipping_issue",
"priority": "P3",
"status": "open",
"order_id": "ACME-0001",
"created_at": "2026-10-25T18:57:44.745Z",
"updated_at": "2026-10-25T18:57:44.745Z",
"resolved_at": null
}
}List your tickets
GET/api/inbound/{partnerSlug}/tickets
Parameters
partnerSlugstringpathrequiredThe partner's Axiom-issued slug (e.g. acme-health).
statusstringqueryoptionalOne of: open, in_progress, resolved.
limitintegerqueryoptionalFrom 1 to 100. Default 50.
offsetintegerqueryoptionalAt least 0. Default 0.
Responses
| Status | Meaning |
|---|---|
200 | Your tickets, most recently updated first |
400 | The request was malformed. validation_failed means the body or query failed schema validation (see issues). On a mutating endpoint you can also get idempotency_key_required (no Idempotency-Key header), idempotency_key_invalid (empty, or longer than 128 characters), or idempotency_key_mismatch (the header and the body's idempotency_key are different strings). |
401 | Missing, expired, or invalid HMAC signature / timestamp |
404 | No such resource for your account in this environment. On order endpoints error is not_found on GET and order_not_found on PATCH and cancel. On the ticket endpoints not_found also means support tickets are not enabled for your account. |
429 | Per-partner rate limit exceeded |
Response fields (200)
oktruerequired
ticketsarray of objectrequired
tickets[].idintegerrequired
tickets[].subjectstringrequired
tickets[].categorystringrequired
tickets[].prioritystringrequiredOne of: P1, P2, P3.
tickets[].statusstringrequiredOne of: open, in_progress, resolved.
tickets[].order_idstring | nullrequiredYour own order reference, or null when the ticket names no order.
tickets[].created_atstring (date-time)required
tickets[].updated_atstring (date-time)required
tickets[].resolved_atstring (date-time) | nulloptional
totalintegerrequired
limitintegerrequired
offsetintegerrequired
Request
http
GET /api/inbound/acme-health/tickets
Host: api.axiomscript.com
X-Axiom-Timestamp: 1767225600
X-Axiom-Signature: sha256=<hex>Response 200
json
{
"ok": true,
"tickets": [
{
"id": 7798,
"subject": "Package arrived damaged",
"category": "shipping_issue",
"priority": "P3",
"status": "resolved",
"order_id": "ACME-0001",
"created_at": "2026-10-25T18:57:44.745Z",
"updated_at": "2026-10-25T18:57:47.052Z",
"resolved_at": "2026-10-25T18:57:47.052Z"
}
],
"total": 1,
"limit": 50,
"offset": 0
}Read a ticket and its thread
GET/api/inbound/{partnerSlug}/tickets/{ticketId}
The whole conversation, oldest first. The first entry is the opening message: what you filed, or, on a thread Axiom opened for you, Axiom's question (author: axiom). author is either partner (you) or axiom (us); individual staff names are not part of this API.
Parameters
partnerSlugstringpathrequiredThe partner's Axiom-issued slug (e.g. acme-health).
ticketIdintegerpathrequiredThe ticket id returned when you opened it. At least 1.
Responses
| Status | Meaning |
|---|---|
200 | The ticket and its messages |
401 | Missing, expired, or invalid HMAC signature / timestamp |
404 | No such resource for your account in this environment. On order endpoints error is not_found on GET and order_not_found on PATCH and cancel. On the ticket endpoints not_found also means support tickets are not enabled for your account. |
429 | Per-partner rate limit exceeded |
Response fields (200)
oktruerequired
ticketobjectrequired
ticket.idintegerrequired
ticket.subjectstringrequired
ticket.categorystringrequired
ticket.prioritystringrequiredOne of: P1, P2, P3.
ticket.statusstringrequiredOne of: open, in_progress, resolved.
ticket.order_idstring | nullrequiredYour own order reference, or null when the ticket names no order.
ticket.created_atstring (date-time)required
ticket.updated_atstring (date-time)required
ticket.resolved_atstring (date-time) | nulloptional
messagesarray of objectrequired
messages[].idintegerrequired0 on the opening post, which is the ticket description rather than a stored reply. The opening post's author is partner for a ticket you filed and axiom for a ticket Axiom opened for you (a question forwarded from a pharmacy or clinic thread about one of your orders); reply to it like any other ticket.
messages[].authorstringrequiredWho wrote it. partner is you. One of: partner, axiom.
messages[].bodystringrequired
messages[].created_atstring (date-time)required
attachmentsarray of objectrequiredEvery file on the thread. Download one with GET /attachments/{attachmentId}.
attachments[].idintegerrequired
attachments[].message_idintegerrequiredThe message it belongs to; 0 is the opening post.
attachments[].file_namestring | nullrequired
attachments[].mime_typestringrequired
attachments[].size_bytesintegerrequired
Request
http
GET /api/inbound/acme-health/tickets/{ticketId}
Host: api.axiomscript.com
X-Axiom-Timestamp: 1767225600
X-Axiom-Signature: sha256=<hex>Response 200
json
{
"ok": true,
"ticket": {
"id": 7798,
"subject": "Package arrived damaged",
"category": "shipping_issue",
"priority": "P3",
"status": "open",
"order_id": "ACME-0001",
"created_at": "2026-10-25T18:57:44.745Z",
"updated_at": "2026-10-25T18:57:45.548Z",
"resolved_at": null
},
"messages": [
{
"id": 0,
"author": "partner",
"body": "The clinic reports the outer box was crushed.",
"created_at": "2026-10-25T18:57:44.745Z"
},
{
"id": 3346,
"author": "partner",
"body": "Photo of the box is attached on our side; please advise.",
"created_at": "2026-10-25T18:57:45.369Z"
}
],
"attachments": []
}Reply on a ticket
POST/api/inbound/{partnerSlug}/tickets/{ticketId}/messages
Adds your message to the thread. Replying to a ticket we have marked resolved REOPENS it and hands it back to us, so you never have to open a second ticket to say the problem is still there.
Parameters
partnerSlugstringpathrequiredThe partner's Axiom-issued slug (e.g. acme-health).
ticketIdintegerpathrequiredThe ticket id returned when you opened it. At least 1.
Idempotency-KeystringheaderrequiredREQUIRED on every endpoint that creates or changes something (see the Idempotency section of the API description for the list). Omitting it is a 400 idempotency_key_required. Replaying the same key with the SAME body returns the original response. Reusing a key with a DIFFERENT body is a 409 idempotency_key_reused. Max 128 characters.
Request body
messagestringrequired1 to 10000 characters.
attachmentsarray of objectoptionalUpload tokens from POST /uploads, each used once. At most 4 items.
attachments[].tokenstringrequiredThe upload.token from POST /uploads. Valid for 24 hours, usable once. 1 to 200 characters.
attachments[].file_namestringoptionalThe name to show for the file. Sent here, in the body, never in a URL or header. 1 to 200 characters.
Responses
| Status | Meaning |
|---|---|
201 | Message added (or idempotent replay) |
400 | The request was malformed. validation_failed means the body or query failed schema validation (see issues). On a mutating endpoint you can also get idempotency_key_required (no Idempotency-Key header), idempotency_key_invalid (empty, or longer than 128 characters), or idempotency_key_mismatch (the header and the body's idempotency_key are different strings). |
401 | Missing, expired, or invalid HMAC signature / timestamp |
404 | ticket_not_found (no such ticket of yours, including a malformed id) or not_found (support tickets are not enabled for your account). |
409 | idempotency_key_reused (this key was already used with a different body) or idempotency_key_in_flight (an identical request with this key is still being processed, so retry). Neither has written anything you do not already have. |
429 | Per-partner rate limit exceeded |
Response fields (201)
oktruerequired
messageobjectrequiredConfirmation that your reply was recorded. Deliberately does NOT echo the text back (so the stored replay of this response holds no ticket text). Read the thread if you need the message rendered alongside ours.
message.idintegerrequired
message.authorstringrequiredOne of: partner.
message.created_atstring (date-time)required
Request
http
POST /api/inbound/acme-health/tickets/{ticketId}/messages
Host: api.axiomscript.com
Content-Type: application/json
X-Axiom-Timestamp: 1767225600
X-Axiom-Signature: sha256=<hex>
Idempotency-Key: <your key>json
{
"message": "Photo of the box is attached on our side; please advise."
}Response 201
json
{
"ok": true,
"message": {
"id": 3346,
"author": "partner",
"created_at": "2026-10-25T18:57:45.369Z"
}
}Resolve a ticket
POST/api/inbound/{partnerSlug}/tickets/{ticketId}/resolve
Marks one of your tickets resolved, for when the problem is sorted on your side. The body is empty. A ticket that is already resolved answers 422 invalid_status_transition. Replying to a resolved ticket reopens it, as it always has, so you never need a separate "reopen" call. You get the usual ticket.status_changed webhook, the same one a resolve from our side sends, and nothing else is sent to anyone. A sandbox key resolves only sandbox tickets.
Parameters
partnerSlugstringpathrequiredThe partner's Axiom-issued slug (e.g. acme-health).
ticketIdintegerpathrequiredThe ticket id returned when you opened it. At least 1.
Idempotency-KeystringheaderrequiredREQUIRED on every endpoint that creates or changes something (see the Idempotency section of the API description for the list). Omitting it is a 400 idempotency_key_required. Replaying the same key with the SAME body returns the original response. Reusing a key with a DIFFERENT body is a 409 idempotency_key_reused. Max 128 characters.
Request body
Responses
| Status | Meaning |
|---|---|
200 | Resolved (or idempotent replay) |
400 | The request was malformed. validation_failed means the body or query failed schema validation (see issues). On a mutating endpoint you can also get idempotency_key_required (no Idempotency-Key header), idempotency_key_invalid (empty, or longer than 128 characters), or idempotency_key_mismatch (the header and the body's idempotency_key are different strings). |
401 | Missing, expired, or invalid HMAC signature / timestamp |
403 | production_not_enabled. You signed with a production key, but Axiom has not yet flipped the production gate on your partner account. Holding a production key is deliberately not enough to place live orders. Contact Axiom to enable it. |
404 | ticket_not_found: no ticket of yours, in this key's environment, has that id. Another partner's ticket, the other environment's, and an id out of range all answer the same. not_found on every path means support tickets are not enabled for your account. |
409 | idempotency_key_reused (this key was already used with a different body) or idempotency_key_in_flight (an identical request with this key is still being processed, so retry). Neither has written anything you do not already have. |
422 | invalid_status_transition: the ticket is already resolved. |
423 | partner_inactive or tenant_suspended. Your partner account is deactivated or suspended, so no request will be served until it is reinstated. |
429 | Per-partner rate limit exceeded |
Response fields (200)
oktruerequired
ticketobjectrequiredConfirmation that the ticket is resolved. Deliberately does NOT echo the subject: read the ticket if you need it.
ticket.idintegerrequired
ticket.statusstringrequiredOne of: resolved.
ticket.resolved_atstring (date-time)required
Request
http
POST /api/inbound/acme-health/tickets/{ticketId}/resolve
Host: api.axiomscript.com
Content-Type: application/json
X-Axiom-Timestamp: 1767225600
X-Axiom-Signature: sha256=<hex>
Idempotency-Key: <your key>json
{}Response 200
json
{
"ok": true,
"ticket": {
"id": 7798,
"status": "resolved",
"resolved_at": "2026-10-25T18:57:47.052Z"
}
}Upload a file, get a one-time attachment token
POST/api/inbound/{partnerSlug}/uploads
Step one of the two-step attachment flow. POST the RAW file bytes as the body (not multipart, not base64): the body IS the file, and the Content-Type header declares its media type. You get back a short-lived token; put it in the attachments array when you open a ticket or reply, and we attach the stored file to that message.
Signing does not change. The signature is over {METHOD}.{path}.{timestamp}.{rawBody} exactly as for a JSON body; here rawBody simply happens to be the file.
The declared Content-Type is checked against the allowlist AND the file's leading bytes are checked against that type's signature, so, for example, a text file declared as a PDF is refused. Office files are checked as ZIP containers only (a DOCX declared as XLSX is not caught). Per-type size limits apply. Do NOT put a real filename in the URL or a header: it can be PHI. Send the display name as attachments[].file_name on the create-ticket or reply call instead.
Parameters
partnerSlugstringpathrequiredThe partner's Axiom-issued slug (e.g. acme-health).
Request body
The raw file bytes. Set Content-Type to the file's actual media type. The allowlist is PDF, PNG, JPG/JPEG, WebP, GIF, HEIC, HEIF, MP4, MOV (QuickTime), WebM, DOCX, XLSX, XLS, CSV, and TXT; anything else is refused with 415. The file's signature bytes are also checked against the declared type (Office files as ZIP containers only).
Sent as application/pdf.
Responses
| Status | Meaning |
|---|---|
201 | Stored. Use upload.token in an attachments array before it expires. |
400 | empty_file: the request body had zero bytes. |
401 | Missing, expired, or invalid HMAC signature / timestamp |
403 | production_not_enabled (you signed with a production key but Axiom has not enabled production for your account) or ip_not_allowlisted (your key has an IP allowlist and this request came from an address not on it). |
404 | not_found: support tickets are not enabled for your partner account. |
413 | file_too_large (over the per-type size limit) or payload_too_large (the raw body is over the hard request ceiling). The size-limit rejections name the cap in maxBytes; the raw body-parser overflow may return only a message. |
415 | unsupported_file_type (the declared Content-Type is not on the allowlist) or file_content_mismatch (the bytes do not match the declared type). |
423 | partner_inactive or tenant_suspended. Your partner account is deactivated or suspended, so no request will be served until it is reinstated. |
429 | Per-partner rate limit exceeded |
500 | upload_failed: the file could not be stored and no token was issued; retry. Removing anything partly stored is best-effort. |
503 | upload_capacity: too many uploads are in flight. A Retry-After header (seconds) says when to try again. |
Response fields (201)
oktruerequired
uploadobjectrequired
upload.tokenstringrequiredOne-time reference to the stored file. Put it in attachments.
upload.size_bytesintegerrequired
upload.expires_atstring (date-time)required
Request
http
POST /api/inbound/acme-health/uploads
Host: api.axiomscript.com
Content-Type: application/pdf
X-Axiom-Timestamp: 1767225600
X-Axiom-Signature: sha256=<hex>Response 201
json
{
"ok": true,
"upload": {
"token": "upl_EXAMPLEtokenEXAMPLEtokenEXAMPLEtoken0000",
"size_bytes": 27,
"expires_at": "2026-10-26T19:16:13.389Z"
}
}Download a ticket attachment
GET/api/inbound/{partnerSlug}/attachments/{attachmentId}
Streams the bytes of an attachment on one of your tickets, governed by the same visibility rule as the thread text. A key cannot fetch bytes from another partner's ticket, or across the environment boundary, by guessing an id: a missing attachment and one that is not yours both return 404, so this is not an existence oracle.
The response is the file with X-Content-Type-Options: nosniff, Cache-Control: private, no-store, and a GENERATED Content-Disposition filename, never the original (which can be PHI).
Parameters
partnerSlugstringpathrequiredThe partner's Axiom-issued slug (e.g. acme-health).
attachmentIdintegerpathrequired
Responses
| Status | Meaning |
|---|---|
200 | The file bytes. Content-Type is the file's stored media type (the same allowlist the upload accepts, e.g. application/pdf, image/png, video/mp4), not octet-stream. |
401 | Missing, expired, or invalid HMAC signature / timestamp |
403 | production_not_enabled (you signed with a production key but Axiom has not enabled production for your account) or ip_not_allowlisted (your key has an IP allowlist and this request came from an address not on it). |
404 | not_found (support tickets are not enabled for your account), attachment_not_found (no such attachment on your tickets, including a malformed id), or attachment_bytes_missing (the record exists but the stored object is gone). |
423 | partner_inactive or tenant_suspended. Your partner account is deactivated or suspended, so no request will be served until it is reinstated. |
500 | attachment_unavailable or attachment_stream_failed: a storage read failed. Nothing is wrong with your request; retry. |
Request
http
GET /api/inbound/acme-health/attachments/{attachmentId}
Host: api.axiomscript.com
X-Axiom-Timestamp: 1767225600
X-Axiom-Signature: sha256=<hex>Mint a session for the embedded support widget
POST/api/inbound/{partnerSlug}/support-sessions
Returns a short-lived URL you drop into an iframe in your own portal, so your staff can open and answer tickets without leaving your product. A session sees EVERY ticket on your account in that environment, with its files: show it only to people allowed to see all of your support threads, never to a clinic or a patient. Call this from YOUR BACKEND: it is signed with your API secret, and the secret must never reach a browser. Pass only the returned embed_url to the page.
The token lives in the URL fragment, which browsers do not send to servers, do not write to access logs, and do not forward in a Referer header. It expires in 15 minutes; mint a new one when a user opens the widget. It carries the environment of the key you signed with, so a sandbox key produces a widget that sees only sandbox tickets.
Before the iframe will render, the origins that may frame it must be on your account's allowlist (a partner admin manages it on the Widget page of the partner portal); anything not on it is refused by the browser.
Parameters
partnerSlugstringpathrequiredThe partner's Axiom-issued slug (e.g. acme-health).
Responses
| Status | Meaning |
|---|---|
201 | A widget session |
401 | Missing, expired, or invalid HMAC signature / timestamp |
404 | No such resource for your account in this environment. On order endpoints error is not_found on GET and order_not_found on PATCH and cancel. On the ticket endpoints not_found also means support tickets are not enabled for your account. |
423 | partner_inactive or tenant_suspended. Your partner account is deactivated or suspended, so no request will be served until it is reinstated. |
429 | Per-partner rate limit exceeded |
Response fields (201)
oktruerequired
embed_urlstringrequiredPut this in an iframe src, unchanged.
tokenstringrequiredThe same credential, if you would rather build the URL yourself.
expires_atstring (date-time)required
Request
http
POST /api/inbound/acme-health/support-sessions
Host: api.axiomscript.com
X-Axiom-Timestamp: 1767225600
X-Axiom-Signature: sha256=<hex>Response 201
json
{
"ok": true,
"embed_url": "https://app.axiomscript.com/embed/support/acme-health#token=EXAMPLE-SESSION-TOKEN",
"token": "EXAMPLE-SESSION-TOKEN",
"expires_at": "2026-10-25T19:31:13.000Z"
}Which domains may embed your support widget, and check one
GET/api/inbound/{partnerSlug}/support-sessions/domains
The domains registered against your account. Enter a ROOT domain with us and every subdomain of it is covered, over https, so you can move the widget between portal., app. and staging. without asking us to change anything.
Read-only here: an API key cannot change the list. A partner admin can, on the Widget page of the partner portal.
Pass ?url= with the page you intend to embed on and we will tell you whether it would be allowed and why. That answer is the point of this endpoint: a browser that refuses to frame a page says nothing useful in the console, so "it is blank and I do not know why" is otherwise the whole debugging experience.
Parameters
partnerSlugstringpathrequiredThe partner's Axiom-issued slug (e.g. acme-health).
urlstringqueryoptionalA page you want to embed the widget on, e.g. https://portal.example.com/support
Responses
| Status | Meaning |
|---|---|
200 | Your registered domains |
401 | Missing, expired, or invalid HMAC signature / timestamp |
404 | No such resource for your account in this environment. On order endpoints error is not_found on GET and order_not_found on PATCH and cancel. On the ticket endpoints not_found also means support tickets are not enabled for your account. |
429 | Per-partner rate limit exceeded |
Response fields (200)
oktruerequired
domainsarray of stringrequired
frame_ancestorsstringrequiredThe literal CSP source list we send, so you can compare it against yours.
notestringrequired
checkobjectoptionalPresent only when you pass ?url=.
check.urlstringrequired
check.allowedbooleanrequired
check.reasonstringrequired
Request
http
GET /api/inbound/acme-health/support-sessions/domains
Host: api.axiomscript.com
X-Axiom-Timestamp: 1767225600
X-Axiom-Signature: sha256=<hex>Response 200
json
{
"ok": true,
"domains": [],
"frame_ancestors": "'none'",
"note": "No domains are registered, so the widget cannot be embedded anywhere yet. Ask us to add yours."
}
