Перейти к содержимому

REST API

Everything the dashboard does, your own backend can do too. Open a ticket when a payment fails, answer it from the tool your team already lives in, keep customer records in step with your database — and be told the moment any of it changes.

Base URL and authentication

Every endpoint lives under https://api.helpwing.app/api/v1, and almost every one is scoped to a project. Server-to-server callers authenticate with a project API key, sent as Authorization: Api-Key hw_live_your_secret_key.

bash
curl -X POST https://api.helpwing.app/api/v1/projects/$HELPWING_PROJECT_ID/tickets/ \
  -H "Authorization: Api-Key $HELPWING_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "subject": "Signup failed",
    "customer_email": "ada@example.com",
    "customer_name": "Ada Lovelace",
    "source": "api",
    "body_text": "Got a 500 on /signup."
  }'

Keys are created under Developers → API keys, by an owner or an admin. Each one belongs to a single project and comes in a test or a live flavour, and the secret is shown exactly once — only its hash is stored, so a key you did not write down is a key you replace. Revoking one takes effect on the next request.

A key belongs on your server. Unlike the widget's public pk_… key, it can read and write the whole project, so it must never reach browser code, a mobile bundle or a repository.

What a key can reach

Tickets, their messages, and customers — the three the API deliberately opens to machines. Everything else (members, billing, domains, widget settings, keys themselves) answers 403 — those exist for people, and they need a signed-in user. A key names its own project, so a request aimed at any other project id is a 404 whether or not that project exists.

Conventions

JSON in and JSON out, UUIDs for ids and ISO 8601 for timestamps. Paths end in a slash. Tickets also carry a reference — ten characters with no l, 1, o or 0 in it — which is what a human quotes back at you, and what you should print in your own interface.

Lists are paginated

Every list endpoint answers with the same envelope. Ask for a page with page and size it with page_size, up to 200; the default is 25.

json
{
  "count": 137,
  "page": 1,
  "page_size": 25,
  "total_pages": 6,
  "next": "…?page=2",
  "previous": null,
  "results": [ … ]
}

Ticket lists take status, priority, source, assignee, customer, tag, unassigned, is_spam, created_after and created_before, plus search across reference, subject, customer and message bodies, and ordering (prefix with - to reverse; the default is -last_message_at).

bash
curl -G https://api.helpwing.app/api/v1/projects/$HELPWING_PROJECT_ID/tickets/ \
  -H "Authorization: Api-Key $HELPWING_API_KEY" \
  -d status=open -d priority=urgent \
  -d search=signup -d ordering=-created_at -d page_size=50

Failures all look the same

Any error, on any endpoint, is one envelope with a stable machine-readable code. Switch on that rather than on the message, which is written for a person and may be reworded.

json
{
  "error": {
    "code": "validation_error",
    "message": "The request payload failed validation.",
    "details": { "customer_email": ["Enter a valid email address."] }
  }
}
400validation_error
The payload failed validation. `details` names the fields.
401authentication_required
No key, an unknown key, or a key that has been revoked.
403permission_denied
A valid key on an endpoint that keys cannot use.
404not_found
No such object in this project, or a project id that is not this key’s.
409conflict
The request contradicts the current state of the object.
429rate_limit_exceeded
Too many requests. Back off and retry.

Opening a ticket

POST /projects/{project_pk}/tickets/ creates the ticket and, when you send a body, its first customer message. Name the customer one of three ways — by id, by email address, or by your own user id — and the record is created or reused rather than duplicated.

subjectstring
What the ticket is called in the inbox. Required.
customer_emailstring
Creates the customer, or reuses the one already holding that address. One of this, customer_id or customer_external_id is required.
customer_iduuid
An existing customer of this project, when you already know which one.
customer_external_idstring
Your own user id. Matches the same customer that identify() in the widget matches, so a ticket you open lands on the record they already have.
customer_namestring
Display name, used only when the customer is new or has none yet.
body_textstring
The first customer message. A ticket without one is an empty conversation.
body_htmlstring
The same message as HTML, when you have it.
prioritystring
low, normal, high or urgent. Defaults to normal.
sourcestring
email, chat or api. Send api — it is what makes the ticket recognisable as yours.
external_refstring
Your identifier for the same thing: an order number, an incident id, a row in your database.
metadataobject
Anything worth seeing beside the ticket. Shown to the agent as it arrives.
tag_idsuuid[]
Tags to apply, by id.

You get the whole ticket back, abridged here. It is already assigned: a new ticket goes to the project's default assignee whichever channel it came from, and they are notified exactly as a human assignment would notify them.

json
{
  "id": "0f5d2a1c-…",
  "reference": "K7X2M9BQTZ",
  "subject": "Signup failed",
  "status": "open",
  "priority": "normal",
  "source": "api",
  "customer": { "id": "…", "email": "ada@example.com", "name": "Ada Lovelace" },
  "assignee": { "id": "…", "name": "Grace Hopper" },
  "message_count": 1,
  "created_at": "2026-08-30T09:12:44Z"
}

Answering one

Post to the ticket's messages. kind: "reply" goes to the customer; kind: "note" is an internal note only your team sees.

bash
curl -X POST https://api.helpwing.app/api/v1/projects/$HELPWING_PROJECT_ID/tickets/<ticket_id>/messages/ \
  -H "Authorization: Api-Key $HELPWING_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "kind": "reply",
    "body_text": "Thanks for the report — deploying a fix now."
  }'

A reply comes back with a delivery_status of queued, or not_applicable when no email is owed — the customer has no address, they are reading the chat widget and the reply reaches them there, or the project has customer email switched off. The ticket's reply_destination says which of those applies before you post.

Attachments belong in the same request, as multipart with one files part per file. Delivery is queued the moment the request commits, so a file uploaded a round trip later is on the ticket but missing from the email that already left.

bash
curl -X POST https://api.helpwing.app/api/v1/projects/$HELPWING_PROJECT_ID/tickets/<ticket_id>/messages/ \
  -H "Authorization: Api-Key $HELPWING_API_KEY" \
  -F kind=reply \
  -F body_text="The log you asked for is attached." \
  -F files=@server.log

Moving it along

Status, priority, assignee and tags each have an endpoint of their own rather than being fields you PATCH — each one records the change in the ticket's activity timeline and fires the matching webhook.

bash
curl -X POST https://api.helpwing.app/api/v1/projects/$HELPWING_PROJECT_ID/tickets/<ticket_id>/status/ \
  -H "Authorization: Api-Key $HELPWING_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"status": "resolved"}'

Webhooks

Polling a ticket list to notice a reply is work you should not have to do. Register an HTTPS endpoint under Developers → Webhooks, choose the events it cares about, and Helpwing posts each one to you as it happens. The endpoint has to be publicly reachable — an address that resolves inside a private network is refused rather than called.

ticket.created
A ticket arrives — by email, from the widget, or from this API.
ticket.updated
Its priority, tags or other fields changed.
ticket.assigned
It was assigned to a member, or unassigned.
ticket.resolved
It was marked resolved.
message.created
A customer message, an agent reply or an internal note was added.
customer.created
A customer record was created for the first time.
email.delivered
A reply was accepted by the receiving mail server.
email.bounced
A reply could not be delivered.

Every event is the same envelope, with the object it concerns under data. The delivery id also arrives as a Helpwing-Delivery header, which is what to store if you want retries to be idempotent.

json
{
  "id": "evt_…",
  "type": "ticket.created",
  "created_at": "2026-08-30T09:12:44Z",
  "project_id": "…",
  "data": {
    "object": "ticket",
    "ticket": { "id": "…", "reference": "K7X2M9BQTZ", "status": "open", … }
  }
}

Check the signature

Your endpoint is a public URL, so anything can post to it. Each delivery carries Helpwing-Signature: t=<timestamp>,v1=<hex>, an HMAC-SHA256 of "<timestamp>.<body>" keyed with the endpoint's signing secret. Recompute it over the raw bytes you were sent, compare in constant time, and reject anything that fails — before you read the payload.

javascript
import { createHmac, timingSafeEqual } from 'node:crypto'

// The raw request body, not a re-serialised object: the digest covers the
// exact bytes that were sent.
export function verifyHelpwingSignature(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map(part => part.split('=')))

  // Reject replays. The timestamp is signed too, so it cannot be edited.
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false

  const expected = createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex')

  return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}

Retries

Answer with any 2xx and the delivery is done. Anything else — a timeout, a 500, a redirect — is retried up to six times with a widening gap, from thirty seconds out to twelve hours, so an endpoint that is down for an afternoon still gets the event. Every attempt, with its request and response, is kept in the delivery log beside the endpoint. An endpoint that fails fifteen times in a row is disabled and stops being called until you turn it back on.

Because a retried delivery is the same event twice, handlers should be idempotent: key on id and ignore one you have already processed.

Every endpoint

The sections above are the parts worth explaining. The rest is the API reference: every operation a key can call, with its parameters, its body and the shape of what it returns — and the same document as an OpenAPI file, if you would rather generate a client than read one. It is generated from the API itself, so it describes what the endpoints do today rather than what they did when somebody last wrote about them.

Signed in already? Developers → Overview prints the first request on this page with your own project id already in it, and Developers → Webhooks can send a test event to your endpoint while you are still writing the handler.

NextAPI reference →Every endpoint a key can call, with its parameters, its body and what it returns.