How Bonga fits into your application
Bonga is a chat server that any number of applications plug into. Your application keeps its own users, its own ids and its own way of signing people in. Bonga gives it conversations, messages and files over a REST API and a WebSocket, and tells your server what happens through webhooks.
At a glance
- Base URL
https://bonga.kweepa.com/ api/ v1 - WebSocket
wss://bonga.kweepa.com/ api/ v1/ ws - Authentication
Authorization: Bearer bga_…for your server, a user token for your front end.- Format
- JSON in and out, camelCase, nulls left out, times in ISO-8601 UTC.
- Webhooks
- Signed with HMAC-SHA256, tried again for up to 8 attempts.
- Errors
- RFC 9457 problem details.
-
Your front end sends
A message with a user token, over REST or the WebSocket.
-
Bonga numbers it
Every message gets a
seq, 1, 2, 3 and so on, without gaps. -
Connected people receive it
message.createdgoes to every connection of every member, whichever server they are on. -
Your server is told
A signed webhook says who the message was for and whether each of them was connected.
-
You notify who was offline
Whoever had no connection is the one to send a push notification.
The words the guide uses
- App
- One application that uses Bonga. Everything belongs to exactly one app, and is invisible to every other.
- User
- Known to Bonga by the id your application already has for them. You never store an id of Bonga's. Users are not registered: the first time your server mentions one, by asking for a token or adding them to a conversation, they exist.
- API key
bga_…, for your server. It may do everything within the app. With the headerX-Bonga-User: <user id>it acts as that user.- User token
- For your front end, fetched by your server for one user. It may do what that user may: only the conversations they are in, by the rules of their role.
- Webhook
- How Bonga tells your server what happens, which is how you send push notifications to people who are not connected.
Get an app and an API key
-
Ask Kweepa for an app
Tell us what your application is called and a short key for it, such as
duka. We make the app and send you its first API key. The key is shown once, and only its hash is kept, so keep it where it was put.To rotate a key
Ask for a second key, deploy it, then have the first revoked. A key that is revoked, or reaches its
expiresAt, stops working at once on every node, and the WebSocket connections opened with it are closed with4401within seconds. -
Keep the key on your server
BONGA_API_URL=https://bonga.kweepa.com/api/v1 BONGA_API_KEY=bga_...Every request carries
Authorization: Bearer <credential>. Whatever goes wrong at the door, the caller learns no more than that it was not let in (401). -
Settle your app's rules
An app's rules are set when it is made, and Kweepa changes them with you. With both of the first two switched off, only your server starts conversations: right for an application where who may talk to whom is its own business, such as a customer and their rider, or a buyer and a seller.
Rules of an app Rule Default Meaning allowClientDirecttrueUsers may start a direct conversation with any user of the app. allowClientGroupstrueUsers may create groups. allowUserDirectoryfalseUsers may search and look up all users of the app, not only those they share a conversation with. maxGroupMembers500The most members of a group. maxMessageLength10000Characters in a message. maxFileBytes2621440025 MB. 0switches uploads off.editWindowSecondsnone How long a sender may edit a message. statusACTIVESUSPENDEDturns the whole app away at once: keys, tokens and open connections.
A first conversation, from a terminal
Two users, a conversation and a message, with nothing but your API key. Alice and Bob exist the moment your server names them.
BONGA_API_URL=https://bonga.kweepa.com/api/v1
# Your server: a token for alice (who is created by being named), and one for bob
ALICE_TOKEN=$(curl -s -X POST $BONGA_API_URL/tokens -H "Authorization: Bearer $BONGA_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"userId":"alice","name":"Alice"}' | jq -r .token)
curl -s -X POST $BONGA_API_URL/tokens -H "Authorization: Bearer $BONGA_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"userId":"bob","name":"Bob"}' > /dev/null
# Alice's front end: the conversation with bob (asked for again, it is the same one), and a message
CONVERSATION_ID=$(curl -s -X POST $BONGA_API_URL/conversations -H "Authorization: Bearer $ALICE_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"type":"DIRECT","memberIds":["bob"]}' | jq -r .id)
curl -s -X POST $BONGA_API_URL/conversations/$CONVERSATION_ID/messages -H "Authorization: Bearer $ALICE_TOKEN" \
-H 'Content-Type: application/json' -d '{"text":"Habari Bob!"}'
# Your server, acting as bob: his list, with the unread count and a preview
curl -s $BONGA_API_URL/conversations -H "Authorization: Bearer $BONGA_API_KEY" -H 'X-Bonga-User: bob'
Integrate in three steps
-
Your server hands your front end a token
When a signed-in user opens a page that chats, your server asks for a token and passes it on.
POST /api/v1/tokens Authorization: Bearer bga_… { "userId": "42", "name": "Alice Wanjiku", "avatarUrl": "https://…/alice.png", "ttlSeconds": 3600 }{ "token": "eyJ…", "expiresAt": "2026-10-09T18:45:12Z", "user": { "id": "42", "name": "Alice Wanjiku", … } }- A user id is whatever yours are: letters, digits and
. _ : @ | + = -, up to 128 characters. A number, a UUID, an email address orauth0|5f7c…all do. - The same call keeps the user's name and picture up to date: what is given replaces what is on record, and what is left out stays.
- A token lasts 24 hours unless
ttlSecondsasks otherwise, and 7 days at the most.
- A user id is whatever yours are: letters, digits and
-
Your front end talks to Bonga
The front end calls the REST API with the token, and opens a WebSocket for what happens while the page is open.
const callBonga = (method, path, body) => fetch('https://bonga.kweepa.com/api/v1' + path, { method, headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' }, body: body && JSON.stringify(body), }).then((response) => response.json()); const conversations = await callBonga('GET', '/conversations'); // each with unread count and preview const conversation = await callBonga('POST', '/conversations', { type: 'DIRECT', memberIds: ['43'] }); const messages = await callBonga('GET', `/conversations/${conversation.id}/messages`); // the latest 50, earliest first const webSocket = new WebSocket('wss://bonga.kweepa.com/api/v1/ws'); webSocket.onopen = () => webSocket.send(JSON.stringify({ type: 'auth', token })); webSocket.onmessage = (event) => { const frame = JSON.parse(event.data); if (frame.type === 'message.created') show(frame.data.message); }; webSocket.send(JSON.stringify({ type: 'message.send', ref: '1', conversationId: conversation.id, text: 'Habari!', clientId: crypto.randomUUID() })); -
Your server hears what happens
Register an endpoint and Bonga posts every event of the app to it. The one most applications want is
message.created, which says who the message was for and whether each of them was connected: whoever was not is whom to send a push notification. See webhooks.
Conversations your application owns
Often a conversation belongs to something of yours: an order, a ride, a ticket. Give it that thing's id and never keep one of Bonga's.
# Makes sure of the conversation of order 10247, and of who is in it. Call it as often as you like.
curl -s -X PUT $BONGA_API_URL/conversations/external/order-10247 -H "Authorization: Bearer $BONGA_API_KEY" \
-H 'Content-Type: application/json' -d '{
"title": "Order 10247",
"metadata": { "orderId": 10247 },
"members": [ { "id": "cust-9", "name": "Achieng" }, { "id": "rider-7", "name": "Juma" } ] }'
# A note about the conversation, from the application itself
curl -s -X POST $BONGA_API_URL/conversations/external:order-10247/messages -H "Authorization: Bearer $BONGA_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"text":"Your rider has arrived","systemEvent":"RIDER_ARRIVED","metadata":{"riderId":"rider-7"}}'
# When the order is done: nobody but your server writes in it any more; it can still be read
curl -s -X PATCH $BONGA_API_URL/conversations/external:order-10247 -H "Authorization: Bearer $BONGA_API_KEY" \
-H 'Content-Type: application/json' -d '{"closed":true}'
Wherever the id of a conversation goes, in a path or in a WebSocket frame, external:<your id> goes as well. What comes back always carries the conversation's own id and its externalId. A message may also carry a customType (say order_card) and metadata for your front end to render in its own way. Such a message needs no text.
Credentials and sign-in
Every request carries Authorization: Bearer <credential>. There are five kinds, and three of them are your application's.
| Credential | Who holds it | What it is |
|---|---|---|
API key (bga_…) |
Your server | The app's own server: everything within the app. With X-Bonga-User: <user id> it acts as that user. Limited for the whole app, far above a person. |
| User token | Your front end | A JWT issued by POST /. The user it was issued for, until it expires (24 hours unless asked otherwise, 7 days at most) or the user is signed out everywhere (DELETE /). |
| Identity provider's token | Your front end | An access token of an OpenID Connect provider your app trusts, with the header X-Bonga-App: <app key>. See below. |
| Platform key | Kweepa | Opens the platform API: making apps, their keys and their rules. Nothing inside an app. |
| Dashboard sign-in | A person | An access token of Kweepa Identity, which opens the dashboard's API and nothing else. |
Users who already sign in somewhere
If your users have access tokens of an OpenID Connect provider, such as your own sign-in or Kweepa Identity, Kweepa registers it for your app and no token of Bonga's is needed. This is what is registered:
{ "issuerUri": "https://id.example.com", "audience": "duka-chat" }
The front end then calls with Authorization: Bearer <access token> and X-Bonga-App: duka.
- The token is checked for its signature (against the provider's published keys, asymmetric algorithms only), its issuer, its audience and its times.
- The
audienceis required: a provider issues tokens to other applications too, which are not to sign anybody in here. - The user is created on first sight from the claims
sub,nameandpicture.userIdClaim,nameClaimandavatarClaimname others, andjwkSetUrisays where the keys are when the provider has no discovery document. - The provider's keys are fetched again when a token names one that is not known, at most every 30 seconds, whatever tokens come in.
Whose users are whose
Without a userIdPrefix, the id in the token is the user's id here: right for a provider whose ids are the ones your server knows its users by, as when the provider is your own sign-in. A provider whose users are other people, or a second provider, needs a prefix of its own, such as google:. Its user 123 is then google:123 here, and never the user 123 your server makes tokens for. A prefix is letters, digits and ._-, with a colon at the end, and no two providers of an app share one.
The REST API
Everything is under /. Fields a client does not know are to be ignored: more may be added. "A user" below means a user token, or an API key with X-Bonga-User.
Users
| Call | What it does | Who |
|---|---|---|
POST / |
Issue a user token. Creates or updates the user. | Your server |
PUT / |
Create or update a user (name, avatarUrl, metadata, active). An avatarUrl that is no http or https address is left out. |
Your server |
DELETE / |
Deactivate: they can neither connect nor send. What they wrote stays. | Your server |
DELETE / |
Sign out everywhere: every token issued for them until now is refused, and their connections are closed. A token issued afterwards works. | Your server |
GET / |
A user, with online and lastSeenAt. |
Your server, or a user for those they may know of |
GET / |
Search by name or id. | Your server, or users where the app keeps a directory |
GET / |
Who of some users is online. | Anybody, for those they may know of |
GET /, GET / |
The calling user, and their unread messages and conversations in all. | A user |
GET /, PUT and DELETE / |
Blocking: neither of the two writes in their direct conversation or sees whether the other is online, and nobody is added to a group by somebody they blocked. | A user |
Conversations
| Call | What it does |
|---|---|
POST / |
{"type":"DIRECT", is the one conversation of two users: 201 when made, 200 when it was there. {"type":"GROUP", makes a new group, whose creator owns it. |
PUT / |
Your server: make sure of a group under an id of your own, and of its members. |
GET / |
A user's list: pinned first, then by latest activity, each with me (role, unreadCount, lastReadSeq, muted, pinned), peer (the other person of a direct conversation) and lastMessage. A server gets all conversations of the app, newest first. |
GET /, GET / |
One conversation. |
PATCH / |
Title, description, avatarUrl, metadata, joinPolicy (INVITE, OPEN), postPolicy (MEMBERS, ADMINS), closed. |
DELETE / |
With everything in it, for good. |
GET / |
Each with role, lastReadSeq, lastDeliveredSeq, online. |
POST / |
Add (userIds). shareHistory: false hides what was said before. |
PATCH / |
Change a role. OWNER hands the group over. |
DELETE / |
Remove a member. |
POST /, / |
Join a group that is open to join. Leave a group. |
PUT / |
For oneself: muted, mutedUntil, pinned, archived. |
POST /, / |
Mark read, or received, up to seq (the latest when left out). |
POST / |
Empty the conversation for oneself alone. |
POST / |
Say that one is typing, for a client without a WebSocket. |
Who may do what in a group
| Action | Owner | Admin | Member |
|---|---|---|---|
| Write, react, read | Yes | Yes | Yes, but not where postPolicy is ADMINS |
| Rename, describe, close, set policies, add members | Yes | Yes | No |
| Remove a member | Yes | Plain members | No |
| Delete anybody's message | Yes | Yes | No |
| Change roles, hand over, delete the group | Yes | No | No |
Your server may do all of it in every conversation of its app. A user who is not in a conversation is told 404 about it, whether it exists or not. When an owner leaves, the group goes to the admin, or else the member, of longest standing.
Messages
| Call | What it does |
|---|---|
POST / |
Send: text, attachments (ids of uploaded files), replyTo, mentions, metadata, customType, clientId. A server may add systemEvent. |
GET / |
A stretch, earliest first, with hasMoreBefore and hasMoreAfter. Nothing given: the latest. |
GET / |
Messages whose text contains the phrase, latest first. |
GET, PATCH, DELETE / |
Read, edit (text, metadata), delete for everybody. |
PUT, DELETE / |
React, take back. Answers the message's reactions as they now stand. |
Four things worth knowing
seq- Numbers the messages of a conversation 1, 2, 3 and so on, without gaps. Sort by it. A client that has seen 41 and receives 43 has missed one and fetches
?after=41. A member has read every message with aseqno higher than theirlastReadSeq, so "read by" and "unread" are comparisons. clientId- Makes sending safe to repeat. Give every message an id of the client's own, such as a UUID. Sent again with the same one by the same sender, the message is stored once and the one already there is answered.
customType- One that begins with
app.is your server's: only an API key sends a message of such a type, or edits one, even one sent in a user's name. A client can trust a card of that type to be the app's own. Other types are anybody's. - A deleted message
- Keeps its place and its
seq, withdeleted: trueand nothing else: no text, no files, no reactions.
The notes the server writes itself are messages of type: "SYSTEM" with a systemEvent (MEMBER_ADDED, MEMBER_JOINED, MEMBER_REMOVED, MEMBER_LEFT, CONVERSATION_RENAMED, CONVERSATION_CLOSED, CONVERSATION_REOPENED) and, in metadata, the actor and the users it is about. They have no text: each client says it in its own words and language.
Lists answer { "items": […],. Pass nextCursor back as cursor for the next page. A cursor is opaque. Messages are paged by seq instead.
Live delivery over the WebSocket
wss://. Every frame is a JSON object with a type.
Signing in
- A first frame
{"type":"auth",, with"token":"…"} "app":"<app key>"added for an identity provider's token. This is the way for a browser, since an address ends up in logs. - The
Authorizationheader of the handshake, withX-Bonga-ApporX-Bonga-User, for a client that can set one. ?token=…in the address, with&app=, for a user's token only. An API key there is refused and the connection closed (4401), since every proxy on the way writes the address into its log. Revoke a key that went there.
The server answers {"type":"connected",. A connection that has not signed in within 10 seconds is closed. Shortly before a token runs out the server sends {"type":"auth.expiring",: send an auth frame with a new token for the same user and the connection stays.
What a client sends
A frame may carry a ref of the client's choosing. The answer to it carries the same one.
| Frame | Answer |
|---|---|
{"type":"message.send",, and every other field of POST …/ |
{"type":"ack", |
{"type":"read", |
ack with lastReadSeq, lastDeliveredSeq, unreadCount. |
{"type":"delivered", |
The same. |
{"type":"typing.start",, typing.stop |
None. Passed on at most every two seconds. |
{"type":"presence.subscribe", |
presence.state, then presence.changed as they come and go. Up to 500 users. Those the caller may not know of are left out, and so are those with whom a block stands either way. |
{"type":"presence.unsubscribe", |
ack. |
{"type":"ping"} |
{"type":"pong"}. |
What cannot be done is answered {"type":"error",, with the same statuses and types as the REST API, and the connection stays open.
What a client is told
{"type":"message.created",, to every connection of every user it concerns, whichever node they are connected to.
| Event | Data | To |
|---|---|---|
message.created, message.updated, message.deleted |
message |
Members, the sender's other devices included. updated and deleted only those who can see the message. |
reaction.added, reaction.removed |
conversationId, messageId, seq, emoji, userId, reactions (as they now stand) |
Members who can see the message. |
conversation.created, conversation.updated |
conversation (a new one with its members, when they are few) |
Members. |
conversation.deleted |
conversationId, externalId |
Members. |
member.added |
conversationId, actorId, members |
Members, the new ones included: finding yourself there means a conversation you do not know yet. |
member.removed |
conversationId, userId, actorId, reason (left, removed) |
Members, and whoever was removed. |
member.updated |
conversationId, member |
Members. |
conversation.read |
conversationId, userId, lastReadSeq, readAt |
Members. In a group of over 100, the reader's own devices only. |
conversation.delivered |
conversationId, userId, lastDeliveredSeq |
Members, not in a group of over 100. |
conversation.settings, conversation.cleared |
The user's own settings, and visibleFromSeq |
The user's own devices. |
typing.started, typing.stopped |
conversationId, userId |
The other members. |
presence.changed |
userId, online, lastSeenAt |
Connections that subscribed. |
Events are sent once and kept nowhere. A conversation in an event has no me. A message that answers one the reader cannot see, said before they came in with shareHistory: false or before they cleared the conversation, comes to them without replyTo.
Staying in step
A WebSocket is for what happens while it is open. After connecting, and again after every reconnect:
-
Fetch the list
GET /, for the list with its unread counts.conversations -
Catch up on the conversation on screen
GET …/, untilmessages?after=<the last seq you have> hasMoreAfteris false.
Reconnect with a growing, randomised pause. The server sends protocol pings every 25 seconds. A client that hears nothing for twice that long should reconnect.
Why a connection is closed
| Code | Meaning | What to do |
|---|---|---|
4401 |
Not signed in, or the credentials ran out: a token expired, the user was signed out everywhere, an API key was revoked or reached its expiresAt, or was put in the address. |
Get a new token, reconnect. |
4403 |
The user was deactivated or the app suspended. | Stop. |
4408 |
No credentials within the time allowed. | Send auth sooner. |
4409 |
The user opened more than 10 connections. The oldest goes. | Nothing. |
4413 |
The client fell 512 frames behind. | Reconnect and catch up. |
1001, 1011 |
The server is going away, or failed. | Reconnect. |
Webhooks
Register an endpoint and Bonga posts the events of your app to it.
curl -s -X POST $BONGA_API_URL/webhooks -H "Authorization: Bearer $BONGA_API_KEY" -H 'Content-Type: application/json' \
-d '{"url":"https://api.example.com/hooks/bonga","eventTypes":["message.created"]}'
- The answer holds the endpoint's
secret(whsec_…), shown then and when you ask for a new one (PATCH /withwebhooks/ {webhookId} {"rotateSecret":true}). - Leave
eventTypesout for every event. An app has up to 10 endpoints. GET /shows how the latest deliveries went.webhooks/ {webhookId}/ deliveries POST /sends awebhooks/ {webhookId}/ test webhook.testevent, five at once and ten a minute.- Endpoints are
httpsand public. Redirects are not followed.
What a delivery looks like
POST /hooks/bonga
Content-Type: application/json
X-Bonga-Event: message.created
X-Bonga-Event-Id: 261009174512A1B2C3D4E5F6
X-Bonga-Delivery: 261009174512F6E5D4C3B2A1
X-Bonga-Signature: t=1791564312,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
{ "id": "261009174512A1B2C3D4E5F6", "type": "message.created", "app": "duka",
"createdAt": "2026-10-09T17:45:12.345Z", "data": { … } }
data is what a client gets for the same event, in the table above. The events for clients alone, which are receipts, typing, presence and a user's own settings, are not sent. message.created carries more:
{ "message": { "id": "…", "seq": 7, "text": "@carol tukutane kesho", "sender": { "id": "alice", … }, … },
"conversation": { "id": "…", "type": "GROUP", "title": "Chama", "externalId": null, … },
"recipients": [
{ "userId": "bob", "online": true, "muted": false, "mentioned": false },
{ "userId": "carol", "online": false, "muted": true, "mentioned": true } ] }
recipients is everybody in the conversation but the sender. Somebody with online: false had no connection when the message was sent: that is whom a push notification is for.
Check the signature
v1 is the HMAC-SHA256, in hex, of t, a dot, and the body exactly as it arrived, under the endpoint's secret. Work it out over the raw bytes, before any JSON parsing, and turn away what is more than a few minutes old.
Node
// Express: app.post('/hooks/bonga', express.raw({ type: 'application/json' }), handler)
const crypto = require('crypto');
function verify(rawBody, header, secret) {
const { t: timestamp, v1: signature } = Object.fromEntries(header.split(',').map((part) => part.split('=')));
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = crypto.createHmac('sha256', secret).update(timestamp + '.').update(rawBody).digest('hex');
return signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
Java
static boolean verify(byte[] rawBody, String header, String secret)
throws GeneralSecurityException {
String timestamp = header.substring(2, header.indexOf(','));
String signature = header.substring(header.indexOf("v1=") + 3);
if (Math.abs(Instant.now().getEpochSecond() - Long.parseLong(timestamp)) > 300)
return false;
Mac hmacSha256 = Mac.getInstance("HmacSHA256");
hmacSha256.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
hmacSha256.update((timestamp + ".").getBytes(StandardCharsets.UTF_8));
String expected = HexFormat.of().formatHex(hmacSha256.doFinal(rawBody));
return MessageDigest.isEqual(expected.getBytes(StandardCharsets.UTF_8),
signature.getBytes(StandardCharsets.UTF_8));
}
What to count on
- Answer
2xxwithin 10 seconds. Anything else is tried again with a pause that doubles, from 30 seconds up to an hour, eight attempts in all. Then it is given up and kept for a month to look at (GET /, whosewebhooks/ {webhookId}/ deliveries lastErrorsays what kind of failure it was). A delivery not made within a day is given up however few times it was tried. - An endpoint that keeps failing is paused. After five failed attempts in a row nothing is posted to it for a minute, twice as long after each failure that follows, half an hour at most, and the first attempt that arrives ends it.
GET /showswebhooks pausedUntilmeanwhile. - A delivery may arrive more than once, since your answer can be lost. Every attempt of an event carries the same
id: drop what you have seen. - Deliveries are not ordered. What they report carries its own order: a message its
seq. - An event is queued in the transaction of the change it reports: your server never hears of something that was rolled back and never misses something that was not.
Files
Sending a file takes two steps: upload it, then send a message with its id.
FILE_ID=$(curl -s -X POST $BONGA_API_URL/files -H "Authorization: Bearer $ALICE_TOKEN" -F file=@photo.jpg | jq -r .id)
curl -s -X POST $BONGA_API_URL/conversations/$CONVERSATION_ID/messages -H "Authorization: Bearer $ALICE_TOKEN" \
-H 'Content-Type: application/json' -d "{\"text\":\"Look\",\"attachments\":[\"$FILE_ID\"]}"
{ "id": "…", "name": "photo.jpg", "contentType": "image/jpeg", "kind": "IMAGE", "size": 482113,
"width": 3024, "height": 4032,
"url": "/api/v1/files/…/content?expires=1791568800&signature=…",
"thumbnailUrl": "/api/v1/files/…/thumbnail?expires=1791568800&signature=…",
"urlExpiresAt": "2026-10-09T19:00:00Z" }
- Any file is taken, up to the app's
maxFileBytes, ten to a message. - What a file is, is read from its bytes, not from its name or the type the upload claimed.
kindisIMAGE,VIDEOorAUDIOfor what was recognised as something a browser plays, andFILEfor everything else. Only the first three are shown in place. The rest, an HTML page or an SVG among them, is a download. - Images (JPEG, PNG, GIF) are measured and given a JPEG preview of 320 pixels, turned the way the photo was taken. WebP is measured.
urlandthumbnailUrlneed no credentials, so they work in an<img>or a<video>. They are signed and good for an hour or a little more. Every time a message is read it comes with fresh ones, andGET /gives fresh ones for one file. Ranges are served.files/ {fileId} - Who may read a file: its uploader, anybody who can read a message it is attached to, and your server. A user can attach only a file they uploaded or can already read, which is what forwarding is.
- A file that no message carries is cleared away a day after it was uploaded.
- A user with a token of their own uploads up to 1 GB in 24 hours. The upload that goes past it is still taken, and the next is answered
429until enough of the day has gone by. Your server is not held to it. 507means there is no room for files just now, and503, withRetry-After, that the store cannot be reached. Try again later.
Errors and limits
Every error is an RFC 9457 problem. Match on type, show detail, and quote trace_id when asking why: it finds the request in the server's log, and is your own X-Request-Id when you sent one.
{ "type": "/errors/not-found", "title": "Not Found", "status": 404,
"detail": "Conversation not found: 261009174512A1B2C3D4E5F6", "trace_id": "9f2c41d07a6b3e58" }
| Type | Status | What it means |
|---|---|---|
bad-request |
400 | The request makes no sense as it is. detail says what. |
validation |
400 | With errors: [{ "field": "members[0].id",. |
unauthorized |
401 | No credentials, or not accepted. |
forbidden |
403 | Signed in, and not allowed this. |
not-found |
404 | Not there, or not for the caller to know of: the two are answered alike. |
conflict |
409 | Does not fit what is on record, such as leaving a direct conversation. |
unprocessable |
422 | Well formed and cannot be carried out, such as naming a user who does not exist. |
too-large |
413 | An upload over the limit, or a body larger than the server reads (4 MB of JSON). |
rate-limited |
429 | With Retry-After and retry_after_ms. |
internal-error |
500 | The server's fault. Nothing of its inside is in the answer. |
unavailable |
503 | Something the server needs cannot be reached just now. Try again after Retry-After. |
insufficient-storage |
507 | No room for files on the server just now. Try the upload again later. |
Rate limits
- A user with a token
- Held per user: 30 messages at once and 120 a minute (sending, editing, reacting), 10 uploads at once and 30 a minute and 1 GB of them in 24 hours, 20 other changes at once and 60 a minute.
- Your server with its API key
- Held for the whole app, far above a person, acting as a user or not: 600 messages at once and 6,000 a minute, 120 uploads at once and 1,200 a minute, 600 other changes at once and 6,000 a minute (the tokens it asks for included), and five test deliveries of a webhook at once and ten a minute.
If your app reaches them
They are there so that no app can take the server for itself, however it came by its key. An app that reaches them needs them raised: tell us.
Common questions
Do our users need a Bonga account?
No. Bonga knows them by the ids your application already uses: your server asks for a token for a user, and that user exists from then on. Nobody signs up twice.
Does Bonga send push notifications?
Your server does, with what Bonga tells it. Every new message comes by webhook with its recipients, and whether each was connected, muted or mentioned; whoever was not connected is the one to notify.
Can a conversation belong to an order or a booking?
Yes. Your server opens it under your own id, such as order-10247, with the customer and the rider in it, posts notes into it, and closes it when the order is done. It can still be read afterwards.
Is Bonga an SMS or WhatsApp service?
No. Bonga carries chat inside your own web and mobile apps, between your users and over the internet; it does not send SMS or WhatsApp messages.
Ready to add chat?
Tell us what your application is and who needs to talk to whom. We will make your app and send its first API key.