API reference
One JSON-over-HTTP surface for everything the dashboard can do. Each topic below has its own page with every endpoint's parameters, payload, response, and error codes. New to the concepts? Start with the guide.
AuthenticationProjects & namingQuickstartStreamsPublishConsumeSubscriptionsFailed messages (DLQ)IntrospectionOperationsProjects & API keysAdmin: team, roles, systemErrorsSemantics
Authentication
Every request that sends a body uses Content-Type: application/json; every authenticated request carries one Authorization header. /healthz, /readyz, and /metrics are always open. With no auth configured at all (local dev), the API is open.
| Project API key recommended for services | Created in the portal under a project (vk_… + vs_… secret, shown once). Authorization: Bearer <key>:<secret> (or headers x-api-key / x-api-secret). The key is pinned to its project: every name resolves inside it, other projects are unreachable, and the admin surface is refused. |
| Static token VARISAI_TOKENS | Configured server-side as token:scope+scope. Scopes: read (all GETs — any valid token has it), publish, consume, manage (create/change streams, subscriptions, projects, keys; redrive, seek, pause), admin (implies everything). Authorization: Bearer <token>. |
| Dashboard session people | Browser calls carry the sign-in cookie. What a person can do is their team role's policy — the same five permissions, assigned on Admin → Team. A 403 names the missing permission and the caller's role. |
Projects & naming
Stream and subscription names live inside a project — payments/orders and analytics/orders are different streams, and each project can have its own billing-worker. Message keys are scoped by their stream, so they inherit the project.
- Every /v1/streams/* and /v1/subscriptions/* endpoint accepts ?project=<name>; creates also accept "project" in the body. Omitted = the default project, which always exists.
- Project API keys never pass the parameter — their project is enforced server-side and a ?project= from them is ignored.
- Names (projects, streams, subscriptions) use letters, digits, dot, dash, underscore. Message ids (msg_…) and delivery ids (dlv_…) are globally unique.
Quickstart
These examples use Varisai Cloud at https://varisai.dev. For a self-hosted deployment, replace the base URL with your server address.
# Auth: create an API key under your project in the portal, then
AUTH='Authorization: Bearer vk_yourkey:vs_yoursecret' # omit on an open dev instance
# 1. a subscription with a declared retry policy (project comes from the key)
curl -X POST https://varisai.dev/v1/subscriptions -H "$AUTH" -d '{
"name": "billing-worker", "stream": "orders",
"retry": { "max_attempts": 5, "strategy": "exponential_jitter",
"initial_backoff": "10s", "max_backoff": "1h" }
}'
# 2. publish — the stream is created implicitly in your project
# (same key = strict order; different keys = full parallelism)
curl -X POST https://varisai.dev/v1/streams/orders/messages -H "$AUTH" \
-d '{"key":"tenant_42","payload":{"order_id":8891},"idempotency_key":"order-8891"}'
# 3. consume: lease -> work -> ack (or nack, and the retry policy takes over)
curl -X POST https://varisai.dev/v1/subscriptions/billing-worker/receive -H "$AUTH" \
-d '{"max_messages":10,"wait_seconds":20,"consumer":"worker-1"}'
curl -X POST https://varisai.dev/v1/subscriptions/billing-worker/ack -H "$AUTH" \
-d '{"delivery_ids":["dlv_..."]}'
# Without an API key, name the project explicitly:
curl -X POST 'https://varisai.dev/v1/streams/orders/messages?project=payments' -d '...'Prefer a client? The zero-dependency Node SDK (sdk/js) wraps all of this — new Varisai({ project: "payments" }) scopes every call, and its consume loop makes resolve = ack, throw = nack.
The message envelope — what GETs, receive, and traces return for a message:
{
"id": "msg_1m1b9kb5qvvcqn0tc334f",
"stream": "orders", "project": "payments",
"key": "tenant_42",
"payload": { "order_id": 8891 },
"headers": { "region": "in" },
"idempotency_key": "order-8891-created",
"deliver_at": null, "expires_at": null,
"published_at": "2026-08-31T10:15:00.000Z",
"offset": 41, "status": "available"
}Endpoints by topic
| Streams scope: manage · reads: read | Create, configure, browse, and delete streams — named durable logs with per-stream retention and backlog caps. |
| Publish scope: publish | Publish messages one at a time or in ordered batches, with idempotency keys, headers, scheduled delivery, and expiry. |
| Consume scope: consume | Long-poll receive with per-message leases, ack, nack, and lease extension — per-key ordering enforced broker-side. |
| Subscriptions scope: manage · reads: read | Declare retry policy, rate limits, filters, and push delivery on the subscription — no retry or DLQ code in consumers. |
| Failed messages (DLQ) scope: manage · reads: read | Browse dead letters grouped by error signature, redrive them back to the queue, or discard them for good. |
| Introspection scope: read | Trace any message's full lifecycle, inspect per-key lanes, scheduled messages, retries, and node-wide stats. |
| Operations scope: manage | Operational verbs: skip a poison message, fire backing-off retries immediately, deliver a scheduled message now. |
| Projects & API keys scope: manage · reads: read | Projects namespace your streams and subscriptions; project API keys are pinned to their project and can never leave it. |
| Admin: team, roles, system scope: admin — dashboard sessions only; API keys are refused here | Team members, role policies, effective configuration, and the audit log — dashboard sessions only. |
Errors
Every error, on every endpoint, is the same shape with a meaningful status code:
{ "error": "a stream named orders already exists in project payments" }| 400 | Malformed request — the message names the field and the accepted values |
| 401 | No credentials, or an unknown API key / wrong secret |
| 403 | Authenticated but not allowed: missing scope/permission, or an API key reaching outside its project |
| 404 | No such stream / subscription / message / project in that scope |
| 409 | Conflict: the name already exists, or the state no longer allows the action |
| 413 | Payload over MAX_MESSAGE_BYTES (default 1 MB) |
| 429 | Backpressure: the stream's undelivered backlog crossed its cap — back off and retry |
| 503 | The broker could not reach MongoDB (readyz fails too) |
Semantics worth knowing
- Delivery is at-least-once: make consumers idempotent. idempotency_key deduplicates publishes within a window (default 24h).
- At most one message per (subscription, key) is in flight; a lease that expires without an ack counts as a failed attempt.
- Rate limits (limits.rate_per_second, rate_per_key_per_second) pace delivery, not publishing.
- Acking never deletes: retention (time-based, per stream) does. Replay with seek works within the message retention window — even past the delivery TTL.
- Scheduled delivery (deliver_at) fires at or after the timestamp — the timer loop ticks every second.
- Long-poll receive is woken push-style the moment work lands (publish, retry release, redrive); consumers do not busy-poll.