Full API reference

Every route your Shopify app can call to open tickets, send messages and keep a merchant's inbox live. Call them from your server: your API key must never reach a browser.

Base URL

Base URL
https://api.wishsupport.app/v1

All paths below are relative to it. Requests and responses are JSON with camelCase fields, and every time is an ISO 8601 UTC string.

Authentication

Send both headers on every request. Create a key in your space under Settings → API keys; it is shown once, so store it right away.

Headers
Authorization: Bearer <your API key>
X-Shop-Domain: acme.myshopify.com

Headers

NameTypeDescription
AuthorizationrequiredstringBearer and your API key. The key picks your space.
X-Shop-DomainrequiredstringThe merchant's .myshopify.com domain. Every read and write is limited to this shop.
Set X-Shop-Domain on your server from your own authenticated Shopify session, never from user input. The key works for every shop in your space, so a shop domain taken from a request would let one merchant read another's tickets.

Conventions

  • Lists are paged with cursor and limit; pass back nextCursor until it is null.
  • A ticket that belongs to another shop answers 404, the same as one that does not exist.
  • A new ticket is always accepted, even when your plan's monthly request limit is reached.
  • Branch on the error code, never on message.

Tickets

A ticket is one conversation between a merchant and your support team. Every route is scoped to the shop in X-Shop-Domain.

Create a ticket

POSThttps://api.wishsupport.app/v1/client/tickets

Opens a conversation and writes its first message in one call. Keep ticket.code: every later call uses it.

Body (JSON)

NameTypeDescription
shopNamerequiredstring1 to 120 characters. Saved as shown at the time.
requesterNamerequiredstring1 to 120 characters. The person writing in.
requesterEmailrequiredstringA valid email. Replies are emailed here when unseen.
subjectrequiredstring1 to 200 characters.
messagestringUp to 5,000 characters. Required unless attachments has a file.
attachmentsUploadedAttachment[]Up to 4 files from Upload a file, passed back unchanged. Defaults to [].
planstringUp to 60 characters. The merchant's plan in your app.
prioritynumberInteger 1 to 10. Defaults to 5.

Request

{
  "shopName": "Acme Supplies",
  "requesterName": "Nadia Rahman",
  "requesterEmail": "nadia@acme.example",
  "subject": "COD form not showing at checkout",
  "message": "The form disappeared after the last update.",
  "plan": "growth",
  "priority": 5
}

Response { ticket: Ticket, message: Message }

JSON
201 Created
{
  "ticket": {
    "id": 1042,
    "code": "TCK-A7K2P9X",
    "shopDomain": "acme.myshopify.com",
    "shopName": "Acme Supplies",
    "requesterName": "Nadia Rahman",
    "requesterEmail": "nadia@acme.example",
    "subject": "COD form not showing at checkout",
    "plan": "growth",
    "priority": 5,
    "status": "open",
    "lastActivityAt": "2026-10-21T09:14:03.000Z",
    "lastSenderType": "shop",
    "lastMessage": {
      "id": 5531,
      "senderType": "shop",
      "senderName": "Nadia Rahman",
      "body": "The form disappeared after the last update.",
      "isDeleted": false,
      "createdAt": "2026-10-21T09:14:03.000Z"
    },
    "unread": false,
    "agentSeenMessageId": 0,
    "createdAt": "2026-10-21T09:14:03.000Z",
    "updatedAt": "2026-10-21T09:14:03.000Z"
  },
  "message": {
    "id": 5531,
    "senderType": "shop",
    "senderUserId": null,
    "senderName": "Nadia Rahman",
    "body": "The form disappeared after the last update.",
    "attachments": [],
    "isDeleted": false,
    "deletedAt": null,
    "deletedByType": null,
    "createdAt": "2026-10-21T09:14:03.000Z"
  }
}

List tickets

GEThttps://api.wishsupport.app/v1/client/tickets

This shop's conversations, newest activity first. Each row carries lastMessage, so a preview needs no extra call.

Query parameters

NameTypeDescription
status'open' | 'closed' | 'archived'Omit for everything except archived.
searchstring1 to 200 characters. Matches the code, subject and message text.
unread'1'Only conversations the shop has not read.
cursornumbernextCursor from the previous page.
limitnumber1 to 100. Defaults to 25.

Request

cURL
curl -X GET "https://api.wishsupport.app/v1/client/tickets?status=open&limit=25" \
  -H "Authorization: Bearer $WISHSUPPORT_API_KEY" \
  -H "X-Shop-Domain: acme.myshopify.com"

Response { items: Ticket[], nextCursor: number | null }

JSON
200 OK
{
  "items": [
    {
      "id": 1042,
      "code": "TCK-A7K2P9X",
      "shopDomain": "acme.myshopify.com",
      "shopName": "Acme Supplies",
      "requesterName": "Nadia Rahman",
      "requesterEmail": "nadia@acme.example",
      "subject": "COD form not showing at checkout",
      "plan": "growth",
      "priority": 5,
      "status": "open",
      "lastActivityAt": "2026-10-21T09:14:03.000Z",
      "lastSenderType": "shop",
      "lastMessage": {
        "id": 5531,
        "senderType": "shop",
        "senderName": "Nadia Rahman",
        "body": "The form disappeared after the last update.",
        "isDeleted": false,
        "createdAt": "2026-10-21T09:14:03.000Z"
      },
      "unread": false,
      "agentSeenMessageId": 0,
      "createdAt": "2026-10-21T09:14:03.000Z",
      "updatedAt": "2026-10-21T09:14:03.000Z"
    }
  ],
  "nextCursor": null
}
  • nextCursor is null on the last page.

Count tickets

GEThttps://api.wishsupport.app/v1/client/tickets/counts

Exact counts by status for tab badges, unaffected by page size.

Query parameters

NameTypeDescription
searchstringNarrows the counts exactly as it narrows the list.

Request

cURL
curl -X GET "https://api.wishsupport.app/v1/client/tickets/counts" \
  -H "Authorization: Bearer $WISHSUPPORT_API_KEY" \
  -H "X-Shop-Domain: acme.myshopify.com"

Response { open, closed, archived, total, unread: number }

JSON
200 OK
{
  "open": 12,
  "closed": 143,
  "archived": 27,
  "total": 182,
  "unread": 3
}
  • total is the sum of the three statuses. unread counts open and closed tickets the shop has not read, and is not part of total.

List ticket changes

GEThttps://api.wishsupport.app/v1/client/tickets/updates

Tickets whose status, priority, archive state or the shop's read state changed since a time, oldest first. Use it to catch up after a reconnect.

Query parameters

NameTypeDescription
sincerequiredstringISO 8601 with an offset. Send polledAt from your last call.
afterIdnumberSend it back when the last response returned one.

Request

cURL
curl -X GET "https://api.wishsupport.app/v1/client/tickets/updates?since=2026-10-21T09:00:00.000Z" \
  -H "Authorization: Bearer $WISHSUPPORT_API_KEY" \
  -H "X-Shop-Domain: acme.myshopify.com"

Response { items: Ticket[], deletedCodes: string[], polledAt: string, afterId: number | null }

JSON
200 OK
{
  "items": [
    {
      "id": 1042,
      "code": "TCK-A7K2P9X",
      "shopDomain": "acme.myshopify.com",
      "shopName": "Acme Supplies",
      "requesterName": "Nadia Rahman",
      "requesterEmail": "nadia@acme.example",
      "subject": "COD form not showing at checkout",
      "plan": "growth",
      "priority": 5,
      "status": "open",
      "lastActivityAt": "2026-10-21T09:14:03.000Z",
      "lastSenderType": "shop",
      "lastMessage": {
        "id": 5531,
        "senderType": "shop",
        "senderName": "Nadia Rahman",
        "body": "The form disappeared after the last update.",
        "isDeleted": false,
        "createdAt": "2026-10-21T09:14:03.000Z"
      },
      "unread": false,
      "agentSeenMessageId": 0,
      "createdAt": "2026-10-21T09:14:03.000Z",
      "updatedAt": "2026-10-21T09:14:03.000Z"
    }
  ],
  "deletedCodes": [],
  "polledAt": "2026-10-21T09:14:03.000Z",
  "afterId": null
}
  • Drop any cached ticket whose code is in deletedCodes. Dedupe items by id.

Get a ticket

GEThttps://api.wishsupport.app/v1/client/tickets/{code}

One ticket by its code.

Path parameters

NameTypeDescription
coderequiredstringThe ticket code, for example TCK-A7K2P9X.

Request

cURL
curl -X GET "https://api.wishsupport.app/v1/client/tickets/TCK-A7K2P9X" \
  -H "Authorization: Bearer $WISHSUPPORT_API_KEY" \
  -H "X-Shop-Domain: acme.myshopify.com"

Response Ticket

JSON
200 OK
{
  "id": 1042,
  "code": "TCK-A7K2P9X",
  "shopDomain": "acme.myshopify.com",
  "shopName": "Acme Supplies",
  "requesterName": "Nadia Rahman",
  "requesterEmail": "nadia@acme.example",
  "subject": "COD form not showing at checkout",
  "plan": "growth",
  "priority": 5,
  "status": "open",
  "lastActivityAt": "2026-10-21T09:14:03.000Z",
  "lastSenderType": "shop",
  "lastMessage": {
    "id": 5531,
    "senderType": "shop",
    "senderName": "Nadia Rahman",
    "body": "The form disappeared after the last update.",
    "isDeleted": false,
    "createdAt": "2026-10-21T09:14:03.000Z"
  },
  "unread": false,
  "agentSeenMessageId": 0,
  "createdAt": "2026-10-21T09:14:03.000Z",
  "updatedAt": "2026-10-21T09:14:03.000Z"
}

Close or reopen a ticket

PATCHhttps://api.wishsupport.app/v1/client/tickets/{code}

Sets the status to closed or open. Archiving happens on its own after a quiet period.

Path parameters

NameTypeDescription
coderequiredstringThe ticket code, for example TCK-A7K2P9X.

Body (JSON)

NameTypeDescription
statusrequired'open' | 'closed'The new status.

Request

{
  "status": "closed"
}

Response Ticket

JSON
200 OK
{
  "id": 1042,
  "code": "TCK-A7K2P9X",
  "shopDomain": "acme.myshopify.com",
  "shopName": "Acme Supplies",
  "requesterName": "Nadia Rahman",
  "requesterEmail": "nadia@acme.example",
  "subject": "COD form not showing at checkout",
  "plan": "growth",
  "priority": 5,
  "status": "closed",
  "lastActivityAt": "2026-10-21T09:14:03.000Z",
  "lastSenderType": "shop",
  "lastMessage": {
    "id": 5531,
    "senderType": "shop",
    "senderName": "Nadia Rahman",
    "body": "The form disappeared after the last update.",
    "isDeleted": false,
    "createdAt": "2026-10-21T09:14:03.000Z"
  },
  "unread": false,
  "agentSeenMessageId": 0,
  "createdAt": "2026-10-21T09:14:03.000Z",
  "updatedAt": "2026-10-21T09:14:03.000Z"
}

Mark as read

POSThttps://api.wishsupport.app/v1/client/tickets/{code}/read

Marks the conversation read for the shop. Call it when the merchant actually opens the thread; it also stops the unseen-reply email.

Path parameters

NameTypeDescription
coderequiredstringThe ticket code, for example TCK-A7K2P9X.

Body (JSON)

NameTypeDescription
upToMessageIdnumberA message on this ticket. Omit to mark everything read.

Request

{
  "upToMessageId": 5531
}

Response Ticket

JSON
200 OK
{
  "id": 1042,
  "code": "TCK-A7K2P9X",
  "shopDomain": "acme.myshopify.com",
  "shopName": "Acme Supplies",
  "requesterName": "Nadia Rahman",
  "requesterEmail": "nadia@acme.example",
  "subject": "COD form not showing at checkout",
  "plan": "growth",
  "priority": 5,
  "status": "open",
  "lastActivityAt": "2026-10-21T09:14:03.000Z",
  "lastSenderType": "shop",
  "lastMessage": {
    "id": 5531,
    "senderType": "shop",
    "senderName": "Nadia Rahman",
    "body": "The form disappeared after the last update.",
    "isDeleted": false,
    "createdAt": "2026-10-21T09:14:03.000Z"
  },
  "unread": false,
  "agentSeenMessageId": 0,
  "createdAt": "2026-10-21T09:14:03.000Z",
  "updatedAt": "2026-10-21T09:14:03.000Z"
}
  • The marker only moves forward, so the call is safe to retry.

Mark as unread

DELETEhttps://api.wishsupport.app/v1/client/tickets/{code}/read

Marks the conversation unread again, from your support team's newest reply.

Path parameters

NameTypeDescription
coderequiredstringThe ticket code, for example TCK-A7K2P9X.

Request

cURL
curl -X DELETE "https://api.wishsupport.app/v1/client/tickets/TCK-A7K2P9X/read" \
  -H "Authorization: Bearer $WISHSUPPORT_API_KEY" \
  -H "X-Shop-Domain: acme.myshopify.com"

Response Ticket

JSON
200 OK
{
  "id": 1042,
  "code": "TCK-A7K2P9X",
  "shopDomain": "acme.myshopify.com",
  "shopName": "Acme Supplies",
  "requesterName": "Nadia Rahman",
  "requesterEmail": "nadia@acme.example",
  "subject": "COD form not showing at checkout",
  "plan": "growth",
  "priority": 5,
  "status": "open",
  "lastActivityAt": "2026-10-21T09:14:03.000Z",
  "lastSenderType": "shop",
  "lastMessage": {
    "id": 5531,
    "senderType": "shop",
    "senderName": "Nadia Rahman",
    "body": "The form disappeared after the last update.",
    "isDeleted": false,
    "createdAt": "2026-10-21T09:14:03.000Z"
  },
  "unread": true,
  "agentSeenMessageId": 0,
  "createdAt": "2026-10-21T09:14:03.000Z",
  "updatedAt": "2026-10-21T09:14:03.000Z"
}

Messages

The messages inside one ticket. Messages you send are recorded as sent by the shop.

Read a thread

GEThttps://api.wishsupport.app/v1/client/tickets/{code}/messages

With no cursor, the newest page, newest first. With afterId, only new messages, oldest first.

Path parameters

NameTypeDescription
coderequiredstringThe ticket code, for example TCK-A7K2P9X.

Query parameters

NameTypeDescription
beforeIdnumberOlder history before this message. Not with afterId.
afterIdnumberNew messages after this id. Send 0 for an empty thread.
afterDeletionIdnumberDeletions after this cursor. Needs afterId.
limitnumber1 to 100. Defaults to 50.

Request

cURL
curl -X GET "https://api.wishsupport.app/v1/client/tickets/TCK-A7K2P9X/messages?afterId=5530&afterDeletionId=0" \
  -H "Authorization: Bearer $WISHSUPPORT_API_KEY" \
  -H "X-Shop-Domain: acme.myshopify.com"

Response { items: Message[], deletions: Message[], messageCursor: number, deletionCursor: number }

JSON
200 OK
{
  "items": [
    {
      "id": 5531,
      "senderType": "shop",
      "senderUserId": null,
      "senderName": "Nadia Rahman",
      "body": "The form disappeared after the last update.",
      "attachments": [],
      "isDeleted": false,
      "deletedAt": null,
      "deletedByType": null,
      "createdAt": "2026-10-21T09:14:03.000Z"
    }
  ],
  "deletions": [],
  "messageCursor": 5531,
  "deletionCursor": 0
}
  • Replace a message you hold with its entry in deletions; never remove the row.

Send a message

POSThttps://api.wishsupport.app/v1/client/tickets/{code}/messages

Adds a message from the merchant. Sending to an archived ticket reopens it; a closed ticket must be reopened first.

Path parameters

NameTypeDescription
coderequiredstringThe ticket code, for example TCK-A7K2P9X.

Body (JSON)

NameTypeDescription
bodystringUp to 5,000 characters. Required unless attachments has a file.
attachmentsUploadedAttachment[]Up to 4 files from Upload a file. Defaults to [].

Request

{
  "body": "Here is a screenshot of the checkout page.",
  "attachments": [
    {
      "url": "https://files.example.com/acme.myshopify.com/TCK-A7K2P9X/screenshot.png",
      "filename": "screenshot.png",
      "mimeType": "image/png",
      "sizeBytes": 182044
    }
  ]
}

Response Message

JSON
201 Created
{
  "id": 5534,
  "senderType": "shop",
  "senderUserId": null,
  "senderName": "Nadia Rahman",
  "body": "Here is a screenshot of the checkout page.",
  "attachments": [
    {
      "url": "https://files.example.com/acme.myshopify.com/TCK-A7K2P9X/screenshot.png",
      "filename": "screenshot.png",
      "mimeType": "image/png",
      "sizeBytes": 182044
    }
  ],
  "isDeleted": false,
  "deletedAt": null,
  "deletedByType": null,
  "createdAt": "2026-10-21T09:14:03.000Z"
}

Delete a message

DELETEhttps://api.wishsupport.app/v1/client/tickets/{code}/messages/{messageId}

Deletes one of the shop's own messages. It stays in the thread as a deleted message, so render isDeleted.

Path parameters

NameTypeDescription
coderequiredstringThe ticket code, for example TCK-A7K2P9X.
messageIdrequirednumberThe message to delete.

Request

cURL
curl -X DELETE "https://api.wishsupport.app/v1/client/tickets/TCK-A7K2P9X/messages/5531" \
  -H "Authorization: Bearer $WISHSUPPORT_API_KEY" \
  -H "X-Shop-Domain: acme.myshopify.com"

Response No content

JSON
204 No Content
No body
  • A message from your support team cannot be deleted here: 403 MESSAGE_NOT_DELETABLE.

Files

Upload first, then pass what the upload returns in attachments when you create a ticket or send a message.

Upload a file

POSThttps://api.wishsupport.app/v1/client/attachments

Uploads one file as multipart/form-data with a single file part.

Query parameters

NameTypeDescription
ticketCodestringThe ticket the file belongs to, when it already exists.

Form data

NameTypeDescription
filerequiredfileUp to 10 MB, images up to 5 MB. PNG, JPEG, GIF, WebP, PDF, text, CSV, ZIP, GZIP, TAR or 7z.

Request

cURL
curl -X POST "https://api.wishsupport.app/v1/client/attachments?ticketCode=TCK-A7K2P9X" \
  -H "Authorization: Bearer $WISHSUPPORT_API_KEY" \
  -H "X-Shop-Domain: acme.myshopify.com" \
  -F "file=@screenshot.png"

Response UploadedAttachment

JSON
201 Created
{
  "url": "https://files.example.com/acme.myshopify.com/TCK-A7K2P9X/screenshot.png",
  "filename": "screenshot.png",
  "mimeType": "image/png",
  "sizeBytes": 182044
}
  • File URLs are public but unguessable; do not treat them as private.

Live updates

A WebSocket delivers new messages and ticket changes in under a second, with the same objects these routes return.

Create a socket ticket

POSThttps://api.wishsupport.app/v1/client/socket-tickets

A single-use credential that lets a browser open the WebSocket without ever holding your API key. Mint it on your server and pass it to the page.

Request

cURL
curl -X POST "https://api.wishsupport.app/v1/client/socket-tickets" \
  -H "Authorization: Bearer $WISHSUPPORT_API_KEY" \
  -H "X-Shop-Domain: acme.myshopify.com"

Response { ticket: string, expiresAt: string }

JSON
200 OK
{
  "ticket": "k3J9xQ2mZ7bR4tY8wP1sV6nL",
  "expiresAt": "2026-10-21T09:15:03.000Z"
}
  • Connect with wss://api.wishsupport.app/v1/client/socket?ticket=<ticket> within 60 seconds. Your server can connect with the same two headers instead.

Connect to the WebSocket

Your server connects with the same two headers. A browser connects with a socket ticket instead, so it never holds the key. It is a plain WebSocket, not socket.io.

import WebSocket from 'ws';

const socket = new WebSocket('wss://api.wishsupport.app/v1/client/socket', {
  headers: {
    Authorization: `Bearer ${process.env.WISHSUPPORT_API_KEY}`,
    'X-Shop-Domain': 'acme.myshopify.com',
  },
});

Frames you send

JSON
{ "op": "subscribe", "ticket": "TCK-A7K2P9X" }
{ "op": "unsubscribe", "ticket": "TCK-A7K2P9X" }
{ "op": "ping" }

Every message and ticket change for the shop arrives without subscribing. Subscribe to the thread a merchant has open, and again after every reconnect. Send aping every 30 seconds and treat a missing socket.pong as a dead connection.

Frames you receive

JSON
{
  "event": "message.created",
  "at": "2026-10-21T09:14:03.000Z",
  "ticket": "TCK-A7K2P9X",
  "data": {
    "id": 5532,
    "senderType": "agent",
    "body": "We have shipped a fix."
  }
}
EventdataMeans
message.createdMessageA new message on a ticket, from either side.
message.deletedMessageA message was deleted. Replace your copy; keep the row.
ticket.createdTicketA new conversation for this shop.
ticket.updatedTicketStatus, priority, preview or unread changed.
ticket.archivedTicketThe ticket was archived after a quiet period.
ticket.readTicketThe shop's own read marker moved, for example from another tab.
ticket.seenTicketYour support team read the merchant's messages up to agentSeenMessageId.

Delivery is at most once. After every reconnect, catch up with Read a thread using afterId andList ticket changes, then subscribe again. Dedupe by id: your own writes echo back as events.

Objects

Ticket

Returned by every ticket route and carried by ticket events.

Fields

NameTypeDescription
idnumberUse code in URLs.
codestringFor example TCK-A7K2P9X.
shopDomainstring
shopNamestringAs sent when the ticket was created.
requesterNamestringAs sent when the ticket was created.
requesterEmailstring
subjectstring
planstring | null
prioritynumber1 to 10. Your support team may change it.
status'open' | 'closed' | 'archived'
lastActivityAtstringThe list is sorted by this.
lastSenderType'shop' | 'agent' | nullshop means the merchant is waiting for a reply.
lastMessageLastMessage | nullThe newest message, its body cut to 200 characters.
unreadbooleanThe shop has not read your support team's newest reply.
agentSeenMessageIdnumberThe newest merchant message your support team has seen.
createdAtstring
updatedAtstring

LastMessage

The preview on every Ticket.

Fields

NameTypeDescription
idnumber
senderType'shop' | 'agent' | 'system'
senderNamestringThe name shown when it was sent.
bodystringUp to 200 characters. Empty when deleted or files only.
isDeletedboolean
createdAtstring

Message

One message in a thread.

Fields

NameTypeDescription
idnumberAlso the thread cursor.
senderType'shop' | 'agent' | 'system'Show system as a status line, not a bubble.
senderUserIdnumber | nullnull for shop and system messages.
senderNamestringThe name shown when it was sent.
bodystringEmpty when deleted or files only.
attachmentsUploadedAttachment[]
isDeletedbooleanKeep the row and show it as deleted.
deletedAtstring | null
deletedByType'user' | 'client' | nulluser is your support team, client is the shop.
createdAtstring

UploadedAttachment

What an upload returns, and what a message carries.

Fields

NameTypeDescription
urlstringA public, unguessable URL.
filenamestring
mimeTypestring
sizeBytesnumber

Errors

Every failure returns the same shape. Validation failures add details. Quote requestId when you contact support.

JSON
409
{
  "statusCode": 409,
  "code": "TICKET_CLOSED",
  "message": "This ticket is closed.",
  "requestId": "01JAC7Q2V8T3M5N9P4R6S2W1XY"
}
CodeHTTPWhat to do
VALIDATION_ERROR400A bad body or query, or a missing X-Shop-Domain. Read details.
UNAUTHORIZED401A missing, unknown or revoked key. Do not retry.
MESSAGE_NOT_DELETABLE403Not the shop's own message.
NOT_FOUND404No such ticket for this shop. Another shop's ticket answers the same.
TICKET_CLOSED409Reopen the ticket before sending.
ATTACHMENT_TOO_LARGE413The file is over the size limit.
ATTACHMENT_TYPE_REJECTED400The file type is not accepted.
RATE_LIMIT_EXCEEDED429Back off and retry with jitter.
INTERNAL_ERROR500Safe to retry once. Keep the requestId.

Limits

LimitValue
Requests per API key10 per second
Requests per IP address60 per second
Message body5,000 characters
Files per message4
File size10 MB, images 5 MB
Page sizeUp to 100
WebSocket connections per key and shop10
Socket ticket lifetime60 seconds, single use
Esc