https://waestaos.waestaltd.comSupport tickets API
Lets a connected system (Benofy, Webook, EduMaster...) raise support tickets on behalf of its own users, follow them, pass on the user's rating, and be told by webhook when we reply, resolve or close a ticket. New here? Read API.md first (keys, errors, rate limits, retries). Machine-readable: openapi.json.
Key: wcp_... · Base: https://waestaos.waestaltd.com/api/v1/support
Every ticket records the system it came from, so the case log, response times, satisfaction ratings and upgrade history stay in one professional record.
How it works
- Your system raises a ticket when a user reports a problem (
POST /tickets), with your ownexternal_ref. - Our team replies and resolves it in *Customer care*. You can read it any time, and you are called on your webhook.
- When resolved, the user gets an email from [email protected] with what we did and a link to rate it. You can also collect the rating in your own screens and send it to us.
- A poor rating (1 or 2) re-opens the ticket and alerts our team. A good rating (3 to 5), or a week of silence, closes it.
Ticket status: open → in_progress / awaiting_client → resolved → closed. First-reply targets: urgent 2h · high 8h · normal 24h · low 72h.
Get connected
- Staff: *Customer care → Systems → the system → Connect this system (API)*.
- Choose the client account the tickets belong to (usually the client that owns or runs the system) and, optionally, a webhook address (https).
- Copy the API key and the webhook signing secret at once. They are shown once; only a hash of the key is stored.
- Put both in the connected system's settings, on its server.
A key sees only tickets of its own client account and system.
Raise a ticket
POST /api/v1/support/tickets
| Field | Required | Description |
|---|---|---|
subject | yes | Short title, up to 180 characters |
message | yes | What happened: steps, expected result, actual result, error text (up to 10,000) |
kind | no | issue (default), complaint, question, suggestion. A complaint is at least high priority. |
priority | no | low, normal (default), high, urgent |
category | no | technical (default), general, billing, project, hosting, domain |
external_ref | recommended | Your own id for the problem (letters, numbers and . _ - : # /, up to 80). Makes a retry safe. |
reporter | recommended | The end user: { "name", "email", "phone" }. With an email, replies, the resolution and the feedback request go to that person. |
curl -X POST https://waestaos.waestaltd.com/api/v1/support/tickets \
-H "Authorization: Bearer wcp_YOUR_KEY" -H "Content-Type: application/json" \
-d '{
"subject": "Report will not export",
"message": "Clicking Export on the fees report does nothing. Started this morning.",
"kind": "issue", "priority": "high", "external_ref": "BEN-4471",
"reporter": { "name": "Grace Auma", "email": "[email protected]", "phone": "+256772000111" }
}'
201 (or 200 with "replayed": true when the same external_ref was already used):
{
"ok": true, "replayed": false,
"ticket": {
"id": 12, "ticket_no": "TICKET-2026-00012", "external_ref": "BEN-4471", "subject": "Report will not export",
"status": "open", "kind": "issue", "priority": "high", "system": "Benofy",
"opened_at": "2026-09-28T08:00:00+00:00", "first_response_at": null, "resolved_at": null,
"first_reply_due": "2026-09-28 16:00:00 UTC",
"resolution_type": null, "resolution": null, "feedback_link": null, "feedback": null
}
}
Store ticket_no next to your own record: it is what you use in every other call.
Read tickets
GET /api/v1/support/tickets/{ticket_no}: the ticket plus itsreplies:
{ "ok": true, "ticket": { "ticket_no": "TICKET-2026-00012", "status": "resolved", "resolution_type": "fixed",
"resolution": "We renewed the expired certificate.", "feedback_link": "https://waestaos.waestaltd.com/feedback/3f9c...", "feedback": null,
"replies": [
{ "from": "customer", "message": "Clicking Export does nothing...", "at": "2026-09-28T08:00:00+00:00" },
{ "from": "support", "message": "We are checking.", "at": "2026-09-28T08:40:00+00:00" } ] } }
GET /api/v1/support/tickets?status=open&external_ref=BEN-4471&limit=25: list (newest first, up to 100).statusis one ofopen,in_progress,awaiting_client,resolved,closed.
Internal notes, the internal root cause and staff names are never included. Once resolved, resolution_type (fixed, workaround, advice, upgrade, no_fault, duplicate, wont_fix) and resolution are filled in, and feedback_link is a page where the user can rate the help.
Add a message
POST /api/v1/support/tickets/{ticket_no}/replies { "message": "It is still failing" }
A reply on a resolved or closed ticket re-opens it. Returns the updated ticket.
Pass on a rating
POST /api/v1/support/tickets/{ticket_no}/feedback { "rating": 5, "comment": "Fast and polite" }
rating is 1 to 5. It works once per resolution and only after the ticket is resolved. (422 otherwise.) 1 or 2 re-opens the ticket; 3 to 5 closes it.
Errors
| HTTP | Meaning |
|---|---|
| 401 | Missing, wrong or revoked key (an SMS key does not work here) |
| 404 | No such ticket for this key |
| 422 | Fix the request; error says what (missing subject or message, bad email or phone, bad external_ref, feedback not allowed yet) |
| 429 | Too many requests (120 per minute per key) or too many wrong keys |
Webhooks
The same webhook address also receives **licence.changed** events about the client's subscription (see SYSTEMS_API.md). Check the event field and ignore events you do not handle.
If the key has a webhook address we POST JSON there:
event | When |
|---|---|
ticket.replied | our team replied to the customer (data.message) |
ticket.resolved | the ticket was resolved (ticket.resolution is filled in) |
ticket.closed | closed by staff, or automatically after a week with no reply to the resolution |
ticket.reopened | re-opened by staff, or by a poor rating |
ticket.status_changed | any other status change |
ticket.feedback | a rating was recorded (data.rating, data.comment) |
{ "event": "ticket.resolved", "sent_at": "2026-09-28T09:15:00+00:00",
"ticket": { "ticket_no": "TICKET-2026-00012", "external_ref": "BEN-4471", "status": "resolved", "resolution": "We renewed the expired certificate.",
"feedback_link": "https://waestaos.waestaltd.com/feedback/3f9c..." } }
Headers on every call:
| Header | Use |
|---|---|
X-Waesta-Signature: sha256=<hex> | HMAC-SHA256 of the raw body with your webhook secret. Verify it first. |
X-Waesta-Event | The event name |
X-Waesta-Delivery | A unique id: ignore repeats |
Answer with any 2xx, quickly. Otherwise we retry after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, then mark it failed (staff see failed webhooks on the system's page). The address must be https and public; private and internal addresses are refused. Webhooks are sent within minutes because the scheduler delivers them (run it every 5 minutes).
// PHP receiver
$body = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $body, $webhookSecret);
if (!hash_equals($expected, $_SERVER['HTTP_X_WAESTA_SIGNATURE'] ?? '')) { http_response_code(401); exit; }
$event = json_decode($body, true);
// find your record by $event['ticket']['external_ref'], update its status, show $event['ticket']['resolution']
http_response_code(204);
(A Node.js receiver is in API.md.)
Good practice
- Always send
external_refandreporter: it makes retries safe and lets us answer the person directly. - Write the
messagefor someone who cannot see your screen: what the user did, what they expected, what happened, when it started, any error text. - Use
kind: "complaint"when a customer is unhappy: it gets high priority and management is alerted. - Show
resolutionandfeedback_linkinside your own product when a ticket is resolved, so users can rate help without leaving. - Treat the webhook as a nudge: if one is missed,
GET /tickets/{ticket_no}always tells the truth.