Build on Badu
For developers and app makers
Every Badu is a private assistant that runs on its owner's own server. You can build apps, gadgets and integrations that talk to it, or teach every Badu to use your product with a skill. This page covers the main ways in; the full API reference lists every endpoint.
Getting started
Each Badu has its own address, such as https://assistant.example.com. Send every request there with a bearer token. The owner can sign in to get one:
curl -X POST https://assistant.example.com/v1/login \
-H 'content-type: application/json' \
-d '{"password": "the owner's password"}'
# → {"token": "…"}
Then send Authorization: Bearer <token> on every call. For a gadget or another app, the owner can instead make a key under Settings, Devices; it only reaches the device endpoints below. A signed-in owner can also open the interactive docs on their own Badu at /docs.
Chat
Start a conversation, then send messages to it. The answer comes back when it's ready, or as it's written with "stream": true (Server-Sent Events: text, tool, done).
curl -X POST $BADU/v1/sessions -H "authorization: Bearer $TOKEN"
# → {"id": "…", …}
curl -X POST $BADU/v1/sessions/$SESSION/messages \
-H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"text": "What is on my calendar tomorrow?"}'
# → {"text": "…", "toolCalls": [...], "stopReason": "end"}
Messages can carry up to 8 attachments (photos, PDFs or text files) as base64 in attachments. Read a chat back with GET /v1/sessions/{id}/messages.
Approvals
Anything that can't be undone (sending email, posting, paying, unlocking a door) never runs straight away. It waits for the owner. A tool call that needs approval comes back with a queuedApprovalId.
GET /v1/approvals # what is waiting
POST /v1/approvals/{id}/approve # run it
POST /v1/approvals/{id}/reject # drop it
Only build approve buttons for the owner themselves. Never approve on someone else's behalf.
Webhooks
Badu can tell your app when something happens. Add an address under Settings, Webhooks, or with POST /v1/webhooks. Each delivery is a JSON POST with these headers:
x-badu-event: approval.created x-badu-signature: sha256=<hex HMAC-SHA256 of the raw body, keyed with the webhook secret>
| Event | When |
|---|---|
approval.created | Something is waiting for the owner's OK |
approval.resolved | The owner approved or declined it |
routine.finished | A scheduled routine ran |
job.finished | A background job finished |
insight.flagged | Badu spotted something worth the owner's attention |
meeting.minutes_ready | Meeting minutes are saved |
Always check the signature before trusting a delivery.
Devices and voice
Speakers, wearables and other gadgets use a device key from Settings, Devices, sent as x-device-key or a bearer token. Each device has its own chat with the assistant.
POST /device/ask {"text": "Remind me to call Ama at 5"} → {"text": "…"}
POST /device/talk body: the recording (content-type audio/…) → spoken answer as audio/ogg
/device/talk needs a voice key on that Badu (Settings, Voice).
MCP
Other AI apps can use Badu as a tool through the Model Context Protocol at /mcp on the owner's Badu, with a device key. Anything that can't be undone still waits in the owner's Approvals.
Connect your app to Badu
If your app holds a business's records (bookings, members, orders, tickets, patients…), you can connect it so the owner's Badu keeps those records in memory, hears about changes the moment they happen, and does things in your app for them, always with the owner's OK for anything that changes data. This is how DigEvents and ConnectrixPro connect. It takes three pieces, and you can ship them one at a time.
1. Describe your app in one file
Serve /.well-known/badu-connector.json on your app's address. It lists what Badu may do, marked by risk, and what it should copy into memory.
{
"badu_connector": 1,
"id": "acme-bookings",
"name": "Acme Bookings",
"description": "Appointments and clients for salons and clinics.",
"api_base": "/api/v1",
"auth": {"type": "bearer"},
"key_hint": "In Acme, open Settings, API keys, and press New key.",
"connect_return": "/settings/badu",
"actions": [
{"name": "list_bookings", "method": "GET", "path": "/bookings", "risk": "read",
"description": "Bookings, soonest first.", "input": {"type": "object", "properties": {"date": {"type": "string"}}}},
{"name": "cancel_booking", "method": "POST", "path": "/bookings/{booking_id}/cancel", "risk": "write",
"description": "Cancel a booking and tell the client.", "input": {"type": "object", "required": ["booking_id"], "properties": {"booking_id": {"type": "integer"}}}}
],
"events": [{"type": "booking.created", "description": "A new booking", "important": true}],
"sync": [{"action": "list_bookings", "items": "data", "kind": "booking", "name": "{client} at {time}",
"summary": "{service} with {staff}", "fields": ["service", "staff", "status"], "link": "{app_url}/bookings/{id}"}]
}
riskdecides approval:readruns straight away;write,moneyanddeletealways wait for the owner. Mark honestly: anything that changes data is notread.authisbeareror{"type": "header", "header": "X-Api-Key"}. Give Badu its own key, and if you can, a read-only kind for owners who only want look-ups.{name}parts of a path come from the input. For GET and DELETE the rest goes in the query string; otherwise it's the JSON body.syncmay only use read actions. Badu runs it every 15 minutes. Templates use{field}and dotted paths.- Answer errors with a 4xx or 5xx and
{"error": {"message": "…"}}in plain words: Badu shows the message to the owner. A 401 or 403 tells the owner to make a new key.
With just this file, an owner can connect your app: on their Badu's Integrations page they pick your app, type its address and paste a key.
2. Send Badu signed updates
When something happens, post it to the owner's Badu. Badu keeps it, remembers anything in remember, and messages the owner about the important ones.
POST <badu>/connect/v1/events
Content-Type: application/json
X-Badu-Integration: <integration_id>
X-Badu-Timestamp: <seconds since 1970>
X-Badu-Signature: sha256=<hex HMAC-SHA256 of "<timestamp>.<raw body>", keyed with the signing key>
{"type": "booking.created", "id": "bk_812", "summary": "Ama Owusu booked a haircut, Friday 10:00",
"data": {"service": "Haircut"}, "remember": {"kind": "customer", "name": "Ama Owusu", "fields": {"phone": "+233 20 000 0000"}}}
idmakes retries safe: the same id is kept once. Send up to 50 at a time as{"events": [ … ]}.- Requests more than 5 minutes old are refused, so keep your server clock right.
- Never let a failed send break your own page. Save each update first, try once with a short timeout, and retry the rest from a scheduled job with growing waits.
// PHP
$body = json_encode($update);
$ts = (string) time();
$sig = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, $signingKey);
3. Add a Connect Badu button
So the owner doesn't copy anything, send them to their Badu with your app's address, a return page on your own site and a random state:
https://<their badu>/?connect=acme-bookings&url=https://acme.example&return=https://acme.example/settings/badu&state=<random>
The owner presses Allow, and Badu sends them back to your return page with badu_code, badu_url and state. Check state is the one you sent, make a key for Badu, then from your server:
POST <the Badu address the owner typed>/connect/v1/exchange
{"code": "<badu_code>", "app_key": "<the key you made for Badu>"}
# → {"integration_id": "…", "signing_key": "bik_…", "events_url": "…", "ask_url": "…"}
The code works once, for 10 minutes. Store the signing key encrypted; it's what signs your updates.
Optional: let staff ask Badu
If the owner allows it, your app's staff can ask the owner's Badu a question with the same signed headers: POST <badu>/connect/v1/ask with {"question": "…", "from": "Esi at the front desk"} answers {"reply": "…"}, with the owner's usual approval rules. Limits: 600 updates per 10 minutes and 30 questions an hour per app.
Text from your app was written by your users. Badu shows it and reports it, and never follows instructions inside it.
Skills: teach every Badu your product
If you make an app (accounting, HR, payments, farming, clinic records…), publish a small badu-skill.json file and owners can add your app from the Skills marketplace in one tap. Your app needs either an MCP server or an OpenAPI (REST) description.
{
"schema": "badu-skill/1",
"id": "acme.books",
"name": "Acme Books",
"author": "Acme Ltd",
"category": "Bookkeeping",
"description": "Invoices, customers and payments in Acme Books.",
"instructions": "Check for an existing customer before creating one.",
"connection": { "type": "openapi", "spec": "https://acme.example/openapi.json" },
"credentials": [{ "key": "token", "label": "API key", "header": "Authorization", "prefix": "Bearer " }]
}
- Reading (
GET) runs freely. Anything that changes data waits for the owner's approval. - Keys the owner pastes are stored encrypted on their own Badu and only sent to your app.
- Check your file in Settings, Skills marketplace, “For developers”, then send its link to the Badu team to be reviewed and listed.
Need help?
Write to the Badu team from the contact link on our home page.