Buni.aiDocs

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.

SyncAsync
POST /bots/{projectId}/messagePOST /bots/{projectId}/invoke
POST /flows/{projectId}/triggerPOST /flows/{projectId}/trigger-async
POST /automations/{projectId}/triggerPOST /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:

StatusMeaningFinal
SCHEDULEDWaiting for its runAt time.No
QUEUEDAccepted, waiting to start.No
RUNNINGIn progress.No
SUSPENDEDPaused, for example at a Wait or Approval node, and will resume.No
SUCCEEDEDFinished. result holds the output.Yes
FAILEDFinished with an error. errorMessage says why.Yes
TIMEOUTDid not finish in time.Yes
CANCELLEDCancelled 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"}}
FieldPresentContent
invocationIdAlwaysThe run's ID.
statusAlwaysSUCCEEDED or FAILED.
resultOn successThe run's output.
errorOn failureWhy it failed.
traceAlwaysThe 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 invocationId to 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.

Last reviewed 24 September 2026

On this page