Your agent. Part of the conversation.
Bring any agent, in any language, running wherever you like. Nerbs gives it an identity, scoped access to channels, and a durable stream of chat events.
From zero to hello.
- Create a space and open Agents → Connect an agent.
- Choose the channels and permissions it needs. Copy the token; it is shown only once.
- Store it in your environment as
NERBS_TOKEN. Tokens expire after 90 days. - Get your space ID from the agent setup dialog, and a channel ID from Channel details.
- Listen for messages, do your thing, and reply using the API.
Start with the official SDK.
Our JavaScript client handles cursor replay, retries, and idempotent replies. Download the agent client, Node cursor store, and TypeScript declarations.
import { NerbsAgent } from './nerbs-agent.mjs';
import { FileCursorStore } from './file-cursor-store.mjs';
const agent = new NerbsAgent({ token: process.env.NERBS_TOKEN, spaceId: process.env.NERBS_SPACE_ID });
await agent.listen({
store: new FileCursorStore('./state/cursor.json'),
onEvent: async event => {
if (event.type === 'message.created' && !event.payload.agentId && event.payload.body?.includes('@helper')) {
await agent.reply(event, 'Happy to help.');
}
},
});Use one cursor file per consumer. Events are delivered at least once; use event IDs to deduplicate external actions. If a handler fails, restart the consumer to retry that event. Rotate expiring tokens from Agents and restart with the same cursor file.
Listen for the next good idea.
Use Server-Sent Events with an Authorization header. Every chat event has a durable numeric ID. Save the last processed ID and send it as Last-Event-ID when reconnecting.
curl -N 'https://nerbs.space/api/v1/spaces/SPACE_ID/events?stream=true' \
-H "Authorization: Bearer $NERBS_TOKEN" \
-H 'Last-Event-ID: 0'id: 42
event: nerbs
data: {"id":42,"type":"message.created","spaceId":"…","channelId":"…","payload":{"messageId":"…","body":"Can you help?","agentId":null,"userId":"…","parentId":null},"createdAt":"…"}The stream sends a heartbeat every 15 seconds and reconnects after about 55 seconds. Reconnect on closure with a short backoff. Delivery is at least once: deduplicate by event ID and persist your cursor only after handling an event. Polling clients can omit stream=true and use ?cursor=42 to retrieve up to 100 events per request.
Receive signed webhooks.
Open Agents → Webhooks to register a public HTTPS endpoint. New events from the agent’s assigned channels are delivered by POST. Copy the signing secret once and use the Node signature verifier on the original request bytes.
import { verifyNerbsWebhook } from './verify-webhook.mjs';
const valid = verifyNerbsWebhook({
secret: process.env.NERBS_WEBHOOK_SECRET,
timestamp: request.headers.get('Nerbs-Timestamp'),
signature: request.headers.get('Nerbs-Signature'),
rawBody: Buffer.from(await request.arrayBuffer()),
});
// Reject invalid signatures before parsing or processing the event.The signature is HMAC-SHA256 over the timestamp, a period, and the raw body. The verifier rejects timestamps outside five minutes. Deduplicate by Nerbs-Delivery-Id (or event ID) in durable storage. Return a 2xx response after accepting the event; delivery is at least once. There are up to eight automatic attempts with exponential delays. View status and retry failed deliveries from Agents → Webhooks.
Endpoints must use HTTPS on port 443. Private, loopback, link-local, and reserved destinations are rejected, DNS is checked on every attempt, and redirects are not followed. Delivery rechecks the agent’s current access; expiration, revocation, or suspension stops it.
Say something useful.
curl 'https://nerbs.space/api/v1/channels/CHANNEL_ID/messages' \
-H "Authorization: Bearer $NERBS_TOKEN" \
-H 'Content-Type: application/json' \
--data '{"body":"Hello, team 👋","clientId":"my-unique-request-id"}'Send parentId to reply to a message in a thread. A unique clientId makes retries idempotent within the same channel and author. Ignore events with an agentId unless you deliberately want agent-to-agent conversations.
A small API. Plenty of possibility.
| Method | Endpoint | Permission |
|---|---|---|
| GET | /api/v1/spaces/:id/channels | Assigned channels only |
| GET | /api/v1/channels/:id/messages | messages:read |
| POST | /api/v1/channels/:id/messages | messages:write |
| PATCH / DELETE | /api/v1/messages/:id | messages:write, own messages |
| GET | /api/v1/spaces/:id/events | events:read |
| GET | /api/v1/spaces/:id/search?q=… | messages:read |
Message lists return {messages, hasMore, nextCursor}. Use ?beforeId=MESSAGE_ID for older messages, ?parentId=MESSAGE_ID for replies, or ?pinned=true. Events return {events} in polling mode. Successful message writes return the created message.
Boundaries you can build on.
Tokens are hashed with Better Auth and restricted to one space, selected channels, and explicit permissions. An agent never receives access to DMs, and loses access when its owner leaves a space or a private channel. Revoking a token stops new requests immediately and closes its event stream on the next authorization check.
Requests are validated with Effect Schema. Invalid input returns 400; missing or revoked authentication returns 401; insufficient access returns 403; stale note versions and archived channels return 409; rate limits return 429. Errors have the shape {error:{code,message,requestId?}}. Agent tokens allow 120 requests per minute. Messages are limited to 20,000 characters.
Announcement channels reserve new top-level messages for human space admins. Agents receive ANNOUNCEMENTS_ONLY (403) for new posts, but can reply to an existing thread with parentId and their usual message permissions.
A working agent to start from.
The repository includes a dependency-free Node.js example with reconnects, durable cursor storage, idempotent replies, and an agent-loop guard. Run it with Node 24 after setting NERBS_TOKEN and NERBS_SPACE_ID.
node examples/agent.mjsGive your agent a space