Async runs and callbacks
Queue a bot message, flow or automation, then poll for the result or receive a signed callback at your webhook URL.
Sync endpoints hold the connection open until the run ends. For anything slow, such as an automation that calls other APIs or waits, use the async endpoints: they answer 202 straight away and report the result later.
| Sync | Async |
|---|---|
POST /bots/{projectId}/message | POST /bots/{projectId}/invoke |
POST /flows/{projectId}/trigger | POST /flows/{projectId}/trigger-async |
POST /automations/{projectId}/trigger | POST /automations/{projectId}/trigger-async |
Start an async run
Add webhookUrl (and ideally webhookSecret) to be called back when the run finishes.
curl -X POST "https://www.buni.ai/api/v1/orgs/$ORG_ID/automations/$PROJECT_ID/trigger-async" \
-H "Authorization: Bearer $BUNI_API_TOKEN" \
-H "Content-Type: application/json" \
-H "x-idempotency-key: order-10442-paid" \
-d '{
"event": "order.paid",
"payload": { "orderId": "ORD-10442", "phone": "+233201234567" },
"webhookUrl": "https://api.example.com/buni/callbacks",
"webhookSecret": "'"$BUNI_CALLBACK_SECRET"'"
}'{ "invocationId": "clx9km1a40030ab12cd34ef40", "status": "QUEUED", "scheduledFor": null }/bots/{projectId}/invoke returns the same fields without scheduledFor. To run later instead of now, see Scheduling.
Get the result
You have two options, and can use both.
Poll the invocation
curl "https://www.buni.ai/api/v1/orgs/$ORG_ID/external-invocations/$INVOCATION_ID" \
-H "Authorization: Bearer $BUNI_API_TOKEN"Use the token that started the run. Poll every few seconds until status is final:
| Status | Meaning | Final |
|---|---|---|
SCHEDULED | Waiting for its runAt time. | No |
QUEUED | Accepted, waiting to start. | No |
RUNNING | In progress. | No |
SUSPENDED | Paused, for example at a Wait or Approval node, and will resume. | No |
SUCCEEDED | Finished. result holds the output. | Yes |
FAILED | Finished with an error. errorMessage says why. | Yes |
TIMEOUT | Did not finish in time. | Yes |
CANCELLED | Cancelled before it ran. | Yes |
The response also includes trace, steps, durationMs and callbackAttempts (each delivery of your callback, with the status code your server returned). Secrets in payloads and results are redacted. See the API reference for every field.
Receive a callback
When the run finishes, Buni.ai sends a POST to the callback URL:
POST /buni/callbacks HTTP/1.1
Host: api.example.com
Content-Type: application/json
X-BuniAI-Signature: 5f2b1c0e9a7d4f6b8c3e2a1d0f9e8b7c6a5d4e3f2b1a0c9d8e7f6a5b4c3d2e1f
{"invocationId":"clx9km1a40030ab12cd34ef40","status":"SUCCEEDED","result":{"success":true},"trace":{"runtime":"automation"}}| Field | Present | Content |
|---|---|---|
invocationId | Always | The run's ID. |
status | Always | SUCCEEDED or FAILED. |
result | On success | The run's output. |
error | On failure | Why it failed. |
trace | Always | The execution trace. |
The callback URL is the first of: the request's webhookUrl, the automation Trigger node's Callback Override, the project's Default callback URL. The secret follows the same order. If none is set, no callback is sent and you poll instead.
Callback rules
- HTTPS only. Addresses that resolve to private, loopback or link-local IPs are refused.
- No redirects. Point the URL at the final address.
- Respond with any 2xx within 10 seconds to acknowledge.
- Retries. Otherwise Buni.ai tries up to 3 times in total, waiting 0.5 seconds and then 1 second between attempts.
- At-least-once. A delivery can arrive twice. Use
invocationIdto ignore repeats, and return 2xx for a repeat too. - Sync calls never send a callback; their result is in the response.
Verify the signature
When a callback secret is set, X-BuniAI-Signature is the lowercase hex HMAC-SHA256 of the raw request body, keyed with the secret. There is no sha256= prefix and no timestamp. Compute it over the exact bytes you received, before any JSON parsing, and compare in constant time.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const seen = new Set(); // use your database in production
app.post('/buni/callbacks', express.raw({ type: 'application/json' }), (req, res) => {
const expected = crypto
.createHmac('sha256', process.env.BUNI_CALLBACK_SECRET)
.update(req.body) // Buffer with the raw bytes
.digest('hex');
const received = req.get('X-BuniAI-Signature') ?? '';
const valid =
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!valid) return res.status(401).send('bad signature');
const event = JSON.parse(req.body.toString('utf8'));
if (!seen.has(event.invocationId)) {
seen.add(event.invocationId);
// handle event.status, event.result or event.error
}
res.sendStatus(200);
});
app.listen(3000);This is a different scheme from the Webhook trigger's x-buni-signature, which uses a sha256= prefix and an optional timestamp. See Inbound webhooks.