Bot Studio
Client REST API & webhooks
Read a WRNexus bot's data out over HTTP with a bot-scoped API key — list bots, conversations, contacts and submissions — and receive signed submission.created webhooks with the HMAC-SHA256 signature spec and exponential-backoff retry behaviour.
The Bot Studio client API is the read-only, API-key-authenticated surface your own backend calls to pull a bot’s data out of WRNexus, plus the outbound webhooks WRNexus POSTs to your endpoint when a lead is captured. Together they let you sync conversations, contacts and submissions into your CRM and react to new leads in real time.
Two distinct auth models. This page covers the client API (
/api/v1/bot/*, authenticated with awbk_…bot API key). The chat runtime (/api/v1/chat/*, per-conversation token) is documented under Programmatic chat, and the in-dashboard management endpoints (/api/bots/*) use your session cookie and are not intended for external use.
Base URL & authentication
https://app.wrnexus.com
Every client endpoint is gated by a bot-scoped API key. Mint one in Bot
Studio under the bot’s API keys panel — the raw key (wbk_…) is shown
once at creation and only its hash is stored, so it can never be retrieved or
re-displayed. Present it in either header:
Authorization: Bearer wbk_<your_bot_api_key>
X-API-Key: wbk_<your_bot_api_key>
A key is pinned to one bot (and therefore one workspace and organization): it can only ever read its own bot’s data, never another tenant’s. The resolved scope is applied to every query automatically — you never pass a bot, workspace or organization id.
Scopes. Read endpoints require the bots:read scope. A key created with
no explicit scopes is unrestricted (receives every scope). A key missing the
required scope returns 403.
Rate limits. 300 requests per minute per source IP (checked before the key
is resolved, so it also throttles key-guessing) and 120 requests per minute per
key. Exceeding either returns 429.
Pagination
List endpoints take limit and offset query parameters:
| Param | Default | Range |
|---|---|---|
limit |
50 |
1–200 |
offset |
0 |
≥ 0 |
Every list response echoes the window and the unfiltered total:
{ "conversations": [ … ], "count": 50, "total": 412, "limit": 50, "offset": 0 }
Endpoints
| Method & path | Returns |
|---|---|
GET /api/v1/bot |
The bot this key belongs to. |
GET /api/v1/bot/conversations |
Conversations (paged; status filter). |
GET /api/v1/bot/conversations/{id} |
One conversation. |
GET /api/v1/bot/contacts |
Contacts (paged). |
GET /api/v1/bot/contacts/{id} |
One contact. |
GET /api/v1/bot/submissions |
Lead submissions (paged; formId filter). |
GET /api/v1/bot/submissions/{id} |
One submission. |
Conversations
GET /api/v1/bot/conversations?limit=20&status=open
Authorization: Bearer wbk_…
{
"conversations": [
{
"id": "conv_…",
"botId": "bot_…",
"channelId": null,
"contactId": "contact_…",
"versionId": "ver_…",
"title": "Do you offer same-day delivery?",
"status": "open",
"messageCount": 6,
"startedAt": "2026-06-24T12:00:00Z",
"lastMessageAt": "2026-06-24T12:05:00Z",
"closedAt": null,
"createdAt": "2026-06-24T12:00:00Z",
"updatedAt": "2026-06-24T12:05:00Z"
}
],
"count": 1, "total": 1, "limit": 20, "offset": 0
}
Contacts
GET /api/v1/bot/contacts/{id}
Authorization: Bearer wbk_…
{
"id": "contact_…",
"botId": "bot_…",
"name": "Ajay Ghanwat",
"email": "ajay@example.com",
"phone": "+919561417403",
"channelType": "web",
"externalId": null,
"locale": "en",
"attributes": {},
"lastSeenAt": "2026-06-24T12:05:00Z",
"createdAt": "2026-06-24T12:00:00Z",
"updatedAt": "2026-06-24T12:05:00Z"
}
Submissions
A submission is a completed lead-capture form. fields keeps the ordered,
labelled entries; values is the same data flattened to a key → value map for
convenience.
GET /api/v1/bot/submissions?formId=form_…&limit=50
Authorization: Bearer wbk_…
{
"id": "sub_…",
"botId": "bot_…",
"formId": "form_…",
"conversationId": "conv_…",
"contactId": "contact_…",
"status": "captured",
"fields": [
{ "key": "name", "label": "Full name", "value": "Ajay Ghanwat" },
{ "key": "service", "label": "Service", "value": "Website development" },
{ "key": "city", "label": "City", "value": "Pune" }
],
"values": { "name": "Ajay Ghanwat", "service": "Website development", "city": "Pune" },
"createdAt": "2026-06-24T12:00:00Z"
}
Webhooks
Instead of polling the submissions endpoint, register a webhook and WRNexus will
POST a signed JSON event to your HTTPS endpoint the moment a lead is
captured. Webhooks are managed per bot, in Bot Studio under Webhooks (and via
the session-authenticated /api/bots/{id}/webhooks management endpoints).
Registering an endpoint
When you create or rotate a webhook, WRNexus returns a signing secret
(whsec_…) once — store it, it is never shown again. The secret is held
encrypted at rest (AES-256-GCM); WRNexus keeps it only to sign outgoing
payloads, and your endpoint keeps its copy to verify them.
Subscribe to specific events or leave the event list empty to receive all events. The only event fired today:
| Event | Fires when… |
|---|---|
submission.created |
A lead-capture form is submitted. |
Delivery headers
Every POST carries:
| Header | Value |
|---|---|
Content-Type |
application/json |
User-Agent |
wrnexus-bot-webhooks/1 |
X-WRNexus-Event |
The event type, e.g. submission.created. |
X-WRNexus-Delivery |
A unique delivery id — stable across retries (idempotency). |
X-WRNexus-Signature |
sha256=<hex> — the HMAC of the raw request body. |
Payload envelope
The event is wrapped in a canonical envelope. id equals the
X-WRNexus-Delivery header — use it to deduplicate retries.
{
"id": "9f2c…",
"event": "submission.created",
"botId": "bot_…",
"createdAt": "2026-06-24T12:00:00Z",
"data": {
"id": "sub_…",
"botId": "bot_…",
"formId": "form_…",
"conversationId": "conv_…",
"contactId": "contact_…",
"status": "captured",
"fields": [
{ "key": "name", "label": "Full name", "value": "Ajay Ghanwat" },
{ "key": "service", "label": "Service", "value": "Website development" }
],
"values": { "name": "Ajay Ghanwat", "service": "Website development" },
"createdAt": "2026-06-24T12:00:00Z"
}
}
Verifying the signature
The signature is sha256= followed by the hex-encoded HMAC-SHA256 of the exact
raw request body, keyed by your endpoint’s whsec_… secret. Compute the same
HMAC over the bytes you received and compare in constant time. Reject any request
whose signature does not match.
// Node.js / Express — verify before trusting the body.
import crypto from 'node:crypto';
function verify(rawBody, signatureHeader, secret) {
const expected =
'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(signatureHeader || '');
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// rawBody MUST be the unparsed body string/Buffer, not the re-serialised JSON.
app.post('/webhooks/wrnexus', express.raw({ type: 'application/json' }), (req, res) => {
if (!verify(req.body, req.get('X-WRNexus-Signature'), process.env.WRNEXUS_BOT_WHSEC)) {
return res.status(401).end();
}
const event = JSON.parse(req.body);
// event.id is the idempotency key — ignore one you have already processed.
// … enqueue work, then …
res.status(200).end();
});
# Python — verify before trusting the body.
import hashlib, hmac
def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header or "")
Retry behaviour
Respond with any 2xx status to acknowledge. WRNexus treats any non-2xx response, a timeout, or a transport failure as a failed attempt and retries with exponential backoff:
- Up to 5 attempts total — the first immediate, then retries after 1s, 2s, 4s, 8s (the backoff doubles each time, capped at 60s).
- Retries run in the background, so capturing a submission never blocks on a slow endpoint.
- The
X-WRNexus-Deliveryid stays the same across all attempts of one event — make your handler idempotent and deduplicate on it. - Each attempt is logged with its HTTP status and error; recent attempts are visible per endpoint in Bot Studio, where you can also send a test event.
- A short request timeout (10s) protects against a hung receiver.
Design for at-least-once delivery. Because a slow or briefly-failing endpoint is retried, you may legitimately receive the same
submission.createdevent more than once. Always key your processing on the envelopeid.
Errors
Errors use the standard WRNexus JSON shape (code + message) with the
appropriate HTTP status:
| Status | When |
|---|---|
401 |
Missing or invalid API key. |
403 |
The key is missing the required bots:read scope. |
404 |
The resource does not exist or is outside the key’s bot scope. |
429 |
Per-IP or per-key rate limit exceeded. |