Skip to content

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.

Open 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 ​

partnerSlugstringpathrequired

The partner's Axiom-issued slug (e.g. acme-health).

Idempotency-Keystringheaderrequired

REQUIRED 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 ​

subjectstringrequired

3 to 200 characters.

descriptionstringrequired

What is wrong, in your own words. This is the first message in the thread and we reply to it directly. 1 to 10000 characters.

categorystringoptional

What 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".

prioritystringoptional

P1 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_idstringoptional

YOUR 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 objectoptional

Upload tokens from POST /uploads, each used once. At most 4 items.

attachments[].tokenstringrequired

The upload.token from POST /uploads. Valid for 24 hours, usable once. 1 to 200 characters.

attachments[].file_namestringoptional

The name to show for the file. Sent here, in the body, never in a URL or header. 1 to 200 characters.

Responses ​

StatusMeaning
201Ticket opened (or idempotent replay)
400The 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).
401Missing, expired, or invalid HMAC signature / timestamp
404not_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).
409idempotency_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.
423partner_inactive or tenant_suspended. Your partner account is deactivated or suspended, so no request will be served until it is reinstated.
429Per-partner rate limit exceeded

Response fields (201) ​

oktruerequired

 

order_id_unresolvedtrueoptional

Present 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.prioritystringrequired

One of: P1, P2, P3.

ticket.statusstringrequired

One of: open, in_progress, resolved.

ticket.order_idstring | nullrequired

Your 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 ​

partnerSlugstringpathrequired

The partner's Axiom-issued slug (e.g. acme-health).

statusstringqueryoptional

One of: open, in_progress, resolved.

limitintegerqueryoptional

From 1 to 100. Default 50.

offsetintegerqueryoptional

At least 0. Default 0.

Responses ​

StatusMeaning
200Your tickets, most recently updated first
400The 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).
401Missing, expired, or invalid HMAC signature / timestamp
404No 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.
429Per-partner rate limit exceeded

Response fields (200) ​

oktruerequired

 

ticketsarray of objectrequired

 

tickets[].idintegerrequired

 

tickets[].subjectstringrequired

 

tickets[].categorystringrequired

 

tickets[].prioritystringrequired

One of: P1, P2, P3.

tickets[].statusstringrequired

One of: open, in_progress, resolved.

tickets[].order_idstring | nullrequired

Your 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 ​

partnerSlugstringpathrequired

The partner's Axiom-issued slug (e.g. acme-health).

ticketIdintegerpathrequired

The ticket id returned when you opened it. At least 1.

Responses ​

StatusMeaning
200The ticket and its messages
401Missing, expired, or invalid HMAC signature / timestamp
404No 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.
429Per-partner rate limit exceeded

Response fields (200) ​

oktruerequired

 

ticketobjectrequired

 

ticket.idintegerrequired

 

ticket.subjectstringrequired

 

ticket.categorystringrequired

 

ticket.prioritystringrequired

One of: P1, P2, P3.

ticket.statusstringrequired

One of: open, in_progress, resolved.

ticket.order_idstring | nullrequired

Your 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[].idintegerrequired

0 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[].authorstringrequired

Who wrote it. partner is you. One of: partner, axiom.

messages[].bodystringrequired

 

messages[].created_atstring (date-time)required

 

attachmentsarray of objectrequired

Every file on the thread. Download one with GET /attachments/{attachmentId}.

attachments[].idintegerrequired

 

attachments[].message_idintegerrequired

The 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 ​

partnerSlugstringpathrequired

The partner's Axiom-issued slug (e.g. acme-health).

ticketIdintegerpathrequired

The ticket id returned when you opened it. At least 1.

Idempotency-Keystringheaderrequired

REQUIRED 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 ​

messagestringrequired

1 to 10000 characters.

attachmentsarray of objectoptional

Upload tokens from POST /uploads, each used once. At most 4 items.

attachments[].tokenstringrequired

The upload.token from POST /uploads. Valid for 24 hours, usable once. 1 to 200 characters.

attachments[].file_namestringoptional

The name to show for the file. Sent here, in the body, never in a URL or header. 1 to 200 characters.

Responses ​

StatusMeaning
201Message added (or idempotent replay)
400The 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).
401Missing, expired, or invalid HMAC signature / timestamp
404ticket_not_found (no such ticket of yours, including a malformed id) or not_found (support tickets are not enabled for your account).
409idempotency_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.
429Per-partner rate limit exceeded

Response fields (201) ​

oktruerequired

 

messageobjectrequired

Confirmation 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.authorstringrequired

One 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 ​

partnerSlugstringpathrequired

The partner's Axiom-issued slug (e.g. acme-health).

ticketIdintegerpathrequired

The ticket id returned when you opened it. At least 1.

Idempotency-Keystringheaderrequired

REQUIRED 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 ​

StatusMeaning
200Resolved (or idempotent replay)
400The 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).
401Missing, expired, or invalid HMAC signature / timestamp
403production_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.
404ticket_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.
409idempotency_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.
422invalid_status_transition: the ticket is already resolved.
423partner_inactive or tenant_suspended. Your partner account is deactivated or suspended, so no request will be served until it is reinstated.
429Per-partner rate limit exceeded

Response fields (200) ​

oktruerequired

 

ticketobjectrequired

Confirmation that the ticket is resolved. Deliberately does NOT echo the subject: read the ticket if you need it.

ticket.idintegerrequired

 

ticket.statusstringrequired

One 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 ​

partnerSlugstringpathrequired

The 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 ​

StatusMeaning
201Stored. Use upload.token in an attachments array before it expires.
400empty_file: the request body had zero bytes.
401Missing, expired, or invalid HMAC signature / timestamp
403production_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).
404not_found: support tickets are not enabled for your partner account.
413file_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.
415unsupported_file_type (the declared Content-Type is not on the allowlist) or file_content_mismatch (the bytes do not match the declared type).
423partner_inactive or tenant_suspended. Your partner account is deactivated or suspended, so no request will be served until it is reinstated.
429Per-partner rate limit exceeded
500upload_failed: the file could not be stored and no token was issued; retry. Removing anything partly stored is best-effort.
503upload_capacity: too many uploads are in flight. A Retry-After header (seconds) says when to try again.

Response fields (201) ​

oktruerequired

 

uploadobjectrequired

 

upload.tokenstringrequired

One-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 ​

partnerSlugstringpathrequired

The partner's Axiom-issued slug (e.g. acme-health).

attachmentIdintegerpathrequired

 

Responses ​

StatusMeaning
200The 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.
401Missing, expired, or invalid HMAC signature / timestamp
403production_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).
404not_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).
423partner_inactive or tenant_suspended. Your partner account is deactivated or suspended, so no request will be served until it is reinstated.
500attachment_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 ​

partnerSlugstringpathrequired

The partner's Axiom-issued slug (e.g. acme-health).

Responses ​

StatusMeaning
201A widget session
401Missing, expired, or invalid HMAC signature / timestamp
404No 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.
423partner_inactive or tenant_suspended. Your partner account is deactivated or suspended, so no request will be served until it is reinstated.
429Per-partner rate limit exceeded

Response fields (201) ​

oktruerequired

 

embed_urlstringrequired

Put this in an iframe src, unchanged.

tokenstringrequired

The 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 ​

partnerSlugstringpathrequired

The partner's Axiom-issued slug (e.g. acme-health).

urlstringqueryoptional

A page you want to embed the widget on, e.g. https://portal.example.com/support

Responses ​

StatusMeaning
200Your registered domains
401Missing, expired, or invalid HMAC signature / timestamp
404No 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.
429Per-partner rate limit exceeded

Response fields (200) ​

oktruerequired

 

domainsarray of stringrequired

 

frame_ancestorsstringrequired

The literal CSP source list we send, so you can compare it against yours.

notestringrequired

 

checkobjectoptional

Present 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."
}