API Documentation

Integrate TacoHelp into your apps with our REST API. Available on Pro plans and above.

Authentication

All API requests require a Bearer token. Create an API key in Settings → API Keys after signing in.

curl -H "Authorization: Bearer th_your_api_key" \ https://tacohelp.com/api/external/tickets

Key format: th_ prefix + 32 bytes base64url

Scopes: tickets:read, tickets:write, chat:read, chat:write, or *

Rate Limiting

100 requests per 60-second sliding window per API key. Rate limit info is returned in response headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 97
X-RateLimit-Reset: 1710648000

Webhooks

Register webhook endpoints in Settings → Webhooks to receive real-time events. Payloads are signed with HMAC-SHA256.

Events: ticket.created, ticket.updated, ticket.commented, ticket.resolved, ticket.closed, ticket.reopened, ticket.merged

Signature header: X-Webhook-Signature

Retry policy: 3 attempts with exponential backoff (2s, 8s, 32s)

Circuit breaker: Endpoint disabled after 10 consecutive failures

{ "event": "ticket.created", "timestamp": "2026-03-17T12:00:00.000Z", "data": { "ticket": { "id": "clx...", "ticketNumber": 42, ... } } }

Embeddable Widget

One script tag adds a floating help button with live chat and a ticket form to any site. No API key is exposed in the browser. Get your snippet and widget secret in Settings → Widget.

<script src="https://tacohelp.com/widget.js" data-org="your-org" data-app="myapp" async></script>

data-app labels chats and tickets by product so one help desk can serve many apps.

data-color, data-position ("left"), data-hide-launcher (use your own button with TacoHelp.open()).

JS API: TacoHelp.open("chat"|"ticket"), close(), toggle(), identify(identity), onUnread(fn).

Verified identity (signed-in users)

Sign the user on your server with the widget secret (HMAC-SHA256 over externalId\nemail\nname\nexp, email lowercased) and pass the result as data-identity. Visitors skip the name/email form and agents see a verified badge. A visitor cannot forge another user without the secret.

const exp = Math.floor(Date.now() / 1000) + 43200; const sig = createHmac("sha256", WIDGET_SECRET) .update([user.id, user.email.toLowerCase(), user.name, String(exp)].join("\n")) .digest("hex"); const identity = { externalId: user.id, email: user.email, name: user.name, exp, sig }; // <script src="https://tacohelp.com/widget.js" data-org="your-org" data-identity='${JSON.stringify(identity)}' async></script>

Requesters are contacts. Tickets created by the widget, the public form, email, or POST /api/external/tickets (createdByEmail, optional createdByName, sourceApp, sendReceipt:false) belong to an org-scoped Contact, never a login. Responses still return createdBy / author with isContact: true.

Email threads. Requesters get a receipt and every reply by email, branded with your org name and color; replying to those emails adds a comment to the ticket. Mail to <org>@reply.tacohelp.com opens a ticket.

Public endpoints used by the widget (no API key; per-IP rate limited; every follow-up call needs the X-Visitor-Token returned by start):

POST /api/chat/:org/start, GET|POST /api/chat/:org/messages, POST /api/chat/:org/typing, POST /api/chat/:org/attachments, POST /api/chat/:org/end, POST /api/chat/:org/satisfaction, GET /api/chat/:org/transcript, GET /api/chat/:org/availability, POST /api/tickets/public

Endpoints

GET/api/external/ticketsScope: tickets:read

List tickets for your organization

Query Parameters

statusstringFilter by status (OPEN, IN_PROGRESS, WAITING, RESOLVED, CLOSED)
limitnumberResults per page (default: 50, max: 100)
offsetnumberPagination offset (default: 0)

Response

{
  "tickets": [
    {
      "id": "clx...",
      "ticketNumber": 42,
      "subject": "Login page bug",
      "status": "OPEN",
      "priority": "HIGH",
      "tags": ["bug", "login"],
      "createdAt": "2026-03-17T...",
      "createdBy": { "name": "Sam", "email": "sam@example.com" },
      "assignee": { "name": "Jordan", "email": "dev@example.com" }
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
POST/api/external/ticketsScope: tickets:write

Create a new ticket

Request Body

{
  "subject": "Login page shows blank screen",
  "description": "When I try to log in using Safari...",
  "priority": "HIGH",
  "createdByEmail": "client@example.com",
  "tags": ["bug", "safari"]
}

Response

{
  "id": "clx...",
  "ticketNumber": 43,
  "subject": "Login page shows blank screen",
  "status": "OPEN",
  "priority": "HIGH",
  "source": "API",
  ...
}
GET/api/external/tickets/:ticketIdScope: tickets:read

Get a ticket with comments

Response

{
  "id": "clx...",
  "ticketNumber": 42,
  "subject": "Login page bug",
  "description": "Full description...",
  "comments": [
    {
      "id": "clx...",
      "content": "Thanks for reporting...",
      "author": { "name": "Jordan" },
      "createdAt": "2026-03-17T..."
    }
  ],
  ...
}
PATCH/api/external/tickets/:ticketIdScope: tickets:write

Update a ticket

Request Body

{
  "status": "IN_PROGRESS",
  "priority": "URGENT",
  "assigneeId": "clx...",
  "tags": ["bug", "urgent"]
}

Response

{ "id": "clx...", "status": "IN_PROGRESS", ... }
POST/api/external/tickets/:ticketId/commentsScope: tickets:write

Add a comment to a ticket

Request Body

{
  "content": "We've identified the issue...",
  "authorEmail": "dev@example.com"
}

Response

{
  "id": "clx...",
  "content": "We've identified the issue...",
  "author": { "name": "dev", "email": "dev@example.com" },
  "createdAt": "2026-03-17T..."
}

Chat API

GET/api/external/chat/conversationsScope: chat:read

List chat conversations for your organization

Query Parameters

statusstringFilter by status (active, ended)
limitnumberResults per page (default: 50, max: 100)
offsetnumberPagination offset (default: 0)

Response

{
  "conversations": [
    {
      "id": "clx...",
      "status": "active",
      "visitorName": "Sam",
      "visitorEmail": "sam@example.com",
      "createdAt": "2026-03-17T...",
      "updatedAt": "2026-03-17T..."
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
POST/api/external/chat/conversationsScope: chat:write

Start a new chat conversation

Request Body

{
  "visitorName": "Sam",
  "visitorEmail": "sam@example.com"
}

Response

{
  "id": "clx...",
  "status": "active",
  "visitorName": "Sam",
  "visitorEmail": "sam@example.com",
  "createdAt": "2026-03-17T..."
}
GET/api/external/chat/conversations/:conversationIdScope: chat:read

Get a conversation with its messages

Response

{
  "id": "clx...",
  "status": "active",
  "visitorName": "Sam",
  "visitorEmail": "sam@example.com",
  "messages": [
    {
      "id": "clx...",
      "senderType": "visitor",
      "content": "Hi, I need help with...",
      "createdAt": "2026-03-17T..."
    },
    {
      "id": "clx...",
      "senderType": "agent",
      "senderId": "clx...",
      "content": "Sure, let me look into that.",
      "createdAt": "2026-03-17T..."
    }
  ],
  "createdAt": "2026-03-17T...",
  "updatedAt": "2026-03-17T..."
}
POST/api/external/chat/conversations/:conversationId/messagesScope: chat:write

Send a message in a conversation

Request Body

{
  "content": "Hi, I need help with my account.",
  "senderType": "visitor"
}

Response

{
  "id": "clx...",
  "senderType": "visitor",
  "content": "Hi, I need help with my account.",
  "createdAt": "2026-03-17T..."
}
PATCH/api/external/chat/conversations/:conversationIdScope: chat:write

End a conversation

Request Body

{
  "status": "ended"
}

Response

{
  "id": "clx...",
  "status": "ended",
  "updatedAt": "2026-03-17T..."
}

Error Responses

400Bad request — missing or invalid parameters
401Unauthorized — invalid or missing API key
403Forbidden — insufficient scope
404Not found — resource doesn't exist in your org
429Rate limit exceeded — wait and retry
500Server error — contact support
{ "error": "Invalid API key" }