API & webhooks
Connections › API & webhooks in the left menu is where you connect SemAgent to your own systems: read conversations and customers and send replies with the API, and get notified instantly with webhooks when something happens.
Who can use it?
The screen follows the Settings row in Roles & Permissions: viewing it needs view access, adding keys and webhooks needs edit access. Because a webhook sends conversation content out, setting one up or editing it also needs permission to view Conversations.
API keys
- Press Create key, give the key a name (which system will use it? e.g. “CRM sync”) and choose its permission: Read only or Read and write. Read and write also allows replying to conversations, adding internal notes and marking conversations as resolved.
- The key is shown only now, once: copy it and keep it somewhere safe. If you lose it, create a new one and revoke the old one.
- A key works with the role permissions of the person who created it. Every request checks whether that person's role can see the equivalent panel screen. If their role narrows, so does the key; if the person is deactivated or deleted, the key stops working.
- Revoke turns a key off immediately and for good; the row stays in the list as Revoked.
- Up to 10 active keys per account and 120 requests per minute per key (requests over the limit get HTTP 429). The list shows when each key was last used.
- SemAgent doesn't store the key itself, only a digest that is enough to check it.
Using the API
The address is https://semagent.ai/api/v1. Send the key in the Authorization: Bearer header with every request. Responses are JSON and carry the version they follow in api_version (currently 2026-10-26). Errors look like {"detail": {"code": "…", "message": "…"}}.
curl "https://semagent.ai/api/v1/conversations?status=active&limit=20" \
-H "Authorization: Bearer $SEMAGENT_API_KEY"
curl -X POST "https://semagent.ai/api/v1/conversations/<id>/messages" \
-H "Authorization: Bearer $SEMAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "Hi, your order ships tomorrow."}'
| Endpoint | What it does | Permission |
|---|---|---|
GET /me | Connection test: the account's id and the key's permission. | read |
GET /conversations | The conversation list. Filters: status, channel, limit (1–100, default 50), offset. | read |
GET /conversations/:id | One conversation, with its messages. | read |
POST /conversations/:id/messages | Sends a reply ({"text": "…"}, at most 4,000 characters). It goes to the customer's channel the same way as Send in the panel, taking the conversation over if needed. | read and write |
POST /conversations/:id/notes | Adds an internal note for the team ({"text": "…"}); the customer never sees it. | read and write |
POST /conversations/:id/resolve | Marks the conversation as resolved. | read and write |
GET /contacts | Customers. q (search), limit (1–100), offset. | read |
GET /contacts/:id | One customer: name, email, conversation count, channels, tags, first and last seen. | read |
Webhooks
Add webhook: a public address starting with https:// and the events to be notified about. When those events happen, a signed request (POST, JSON) is sent to your address. Events from the moment you set it up are sent; past conversations aren't replayed.
| Event | When |
|---|---|
conversation.created | A new conversation opens. |
message.created | A message arrives or goes out (customer, team or AI). data.message is the message itself: the author (visitor, agent, ai, system, auto), the text (at most 4,000 characters) and attachments (name and size). |
conversation.escalated | The conversation is handed to the team. |
conversation.resolved | The conversation is resolved. |
csat.submitted | A customer gives a satisfaction rating; data.csat carries the score and the comment. |
Every event's data.conversation sums up the conversation: channel, status, whether it was taken over, the customer's name and email, and when it was created, updated and resolved. A sample message.created request:
{
"id": "4b9e0c52-7a1d-4f3e-9c06-2d8e5a1f7b30",
"event": "message.created",
"api_version": "2026-10-26",
"tenant_id": "00000000-0000-4000-8000-000000000000",
"created_at": "2026-10-26T08:41:10Z",
"data": {
"conversation": {
"id": "0f3c7a91-5e2b-4d8a-b1c4-9a6e2d7f0c11",
"channel": "whatsapp",
"status": "active",
"human_takeover": false,
"customer": { "name": "Elif Yıldız", "email": null },
"created_at": "2026-10-26T08:40:52Z",
"updated_at": "2026-10-26T08:41:09Z",
"resolved_at": null
},
"message": {
"id": "7d21e6b0-3c4f-4a9e-8b57-1e0f9c2a6d48",
"author": "visitor",
"text": "When will my order ship?",
"created_at": "2026-10-26T08:41:09Z",
"attachments": []
}
}
}
- Who it runs as: a webhook runs as the person who set it up (or last changed its address or events, or resumed it). If that person can no longer view Conversations or their account is off, the webhook stops by itself (Paused · creator lacks access); if you resume it, it runs as you.
- Send test sends a
webhook.testevent with a real signature and shows the result (HTTP code and time); tests aren't recorded. - Deliveries lists recent deliveries: the event, status (Delivered, Will retry, Failed, Queued), attempt, HTTP code and time. Delivery records never keep the request body and are deleted after 7 days.
- Success and retries: a 2xx response counts as delivered; the response is awaited for 10 seconds. A failed request is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. After 20 failed deliveries in a row the webhook is paused; fix the issue and press Resume.
- Safe addresses: requests only go to public addresses; addresses that point to a private network are refused and redirects aren't followed.
- Up to 5 webhooks per account.
Verifying the signature
When a webhook is created, its secret is shown once; if you lose it, use Rotate secret. Every request carries three headers: SemAgent-Event (the event), SemAgent-Delivery (the delivery id) and SemAgent-Signature (t=<unix time>,v1=<signature>). The signature is the HMAC-SHA256, with your secret, of the header's t value, a dot and the request body joined together.
- Compute the signature over the raw bytes of the body; don't parse and re-serialise the JSON first.
- Reject requests whose
tis older than 5 minutes. - The same event may rarely arrive twice: deduplicate by the
SemAgent-Deliveryid.
# Python
import hashlib, hmac, time
def verify(secret: str, header: str, body: bytes) -> bool:
# header: SemAgent-Signature = "t=<unix>,v1=<hex>" ; body: the raw request body
parts = dict(p.split("=", 1) for p in header.split(","))
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"]) and abs(time.time() - int(parts["t"])) < 300
// Node.js
const crypto = require("crypto")
function verify(secret, header, rawBody) {
const p = Object.fromEntries(header.split(",").map((x) => x.split("=")))
const expected = crypto.createHmac("sha256", secret).update(`${p.t}.`).update(rawBody).digest("hex")
const same = expected.length === p.v1.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(p.v1))
return same && Math.abs(Date.now() / 1000 - Number(p.t)) < 300
}
Webhooks and the API carry your customers' conversation content into your own systems. Protecting that data there, and describing it in your privacy notice, is your responsibility.
SemAgent