Server API
Plane ③ of the integration contract: your backend talking to Sollar’s servers.
Base URL: https://api.sollar.com/v1
Everything here is Planned — specified, not yet running. Build against it; expect the shapes to
hold and the host to be confirmed at launch.
Authentication
Section titled “Authentication”Every request is signed with HMAC-SHA256 over the raw body.
base = "v0:" + timestamp + ":" + raw_bodysignature = "v0=" + hex(HMAC_SHA256(signing_secret, base))POST /v1/rooms/!abc:sollar.com/cards HTTP/1.1Host: api.sollar.comContent-Type: application/jsonX-Sollar-Request-Timestamp: 1788278400X-Sollar-Signature: v0=8f2c...X-Sollar-Mini-App: com.acme.erpimport { createHmac, timingSafeEqual } from 'node:crypto'
function sign(secret, timestamp, rawBody) { const base = `v0:${timestamp}:${rawBody}` return 'v0=' + createHmac('sha256', secret).update(base).digest('hex')}
function verify(secret, timestamp, rawBody, received) { if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false // replay window const expected = Buffer.from(sign(secret, timestamp, rawBody)) const actual = Buffer.from(received) return expected.length === actual.length && timingSafeEqual(expected, actual)}Three details that are not optional:
- Sign the raw body, before any JSON parsing. A re-serialised body produces a different signature and you will chase it for a day.
- Constant-time comparison.
===on a signature leaks its bytes through timing. - A short timestamp window — five minutes. Without it a captured request replays forever.
Endpoints
Section titled “Endpoints”Post a card into a room
Section titled “Post a card into a room”POST /v1/rooms/{room_id}/cards{ "mini_app_id": "com.acme.erp", "app_user_id": "u_9f3a…", "card": { "title": "Purchase order 4471 approved", "body": "€12,400 · Vendor: Nordwerk GmbH", "actions": [ { "label": "Open", "deep_link": "/orders/4471" } ] }}201 with { "event_id": "…" }.
409 with type: room-encrypted when the room is end-to-end encrypted — see below.
Send a notification
Section titled “Send a notification”POST /v1/users/{app_user_id}/notifications{ "mini_app_id": "com.acme.erp", "title": "Approval needed", "body": "Purchase order 4472 is waiting for you.", "deep_link": "/orders/4472"}Requires the notifications scope, granted by that user. A notification to a user who has not
granted it returns 403, not silence.
Read the mini app’s own installation state
Section titled “Read the mini app’s own installation state”GET /v1/apps/{mini_app_id}/installations/{app_user_id}{ "installed": true, "version": "2.4.1", "granted_scopes": ["identity.basic", "camera"], "tenant_id": "acme-corp"}Useful for deciding whether a notification will land before sending it.
Resolve an agent delegation
Section titled “Resolve an agent delegation”POST /v1/agents/verifyGiven a token carrying an act claim, returns the agent’s identity and the tenant policy that
applies to it — so your backend can enforce a lower limit for agent-initiated actions than for
human ones.
{ "subject": { "app_user_id": "u_9f3a…" }, "actor": { "agent_id": "agent_admin_assistant", "kind": "sollar_agent" }, "tenant_policy": { "max_approval_minor": 500000, "currency_code": "EUR" }}Webhooks
Section titled “Webhooks”Sollar calls your backend for events you subscribe to in the developer console. The signature scheme is identical, and the same three rules apply.
| Event | Sent when |
|---|---|
installation.created | A user installs the mini app |
installation.deleted | Uninstalled, or removed by an administrator |
scope.granted / scope.revoked | A permission changes |
user.deprovisioned | The user leaves the tenant — delete their data |
app.revoked | Sollar has revoked a version. Stop serving it. |
{ "event": "user.deprovisioned", "occurred_at": "2026-09-01T09:14:22Z", "mini_app_id": "com.acme.erp", "app_user_id": "u_9f3a…", "tenant_id": "acme-corp"}Respond 2xx within 5 seconds. Sollar retries with exponential backoff for 24 hours, then stops and
raises an alert in your developer console. Make handlers idempotent — a retry after a timeout on a
request you did process is normal, not exceptional.
user.deprovisioned is the one with a legal deadline attached. Treat it as a deletion instruction,
not a notification.
Encrypted rooms
Section titled “Encrypted rooms”Errors
Section titled “Errors”RFC 9457 problem details:
{ "type": "https://miniapp.sollar.com/errors/scope-not-granted", "title": "Scope not granted", "status": 403, "detail": "The user has not granted 'notifications' to com.acme.erp. Request it in the mini app at the point of use; it cannot be granted from the server.", "instance": "/v1/users/u_9f3a…/notifications"}| Status | Meaning |
|---|---|
400 | Malformed request; detail names the field |
401 | Signature invalid, missing, or outside the timestamp window |
403 | Scope not granted, or tenant policy forbids it |
404 | Unknown room, user or mini app |
409 | State conflict — encrypted room, version revoked |
429 | Rate limited; honour Retry-After |
503 | Retriable |
detail is always a sentence that says what to do. That is a deliberate contract: your logs are
read by humans under time pressure and increasingly by agents deciding what to try next, and neither
learns anything from a bare code.
Rate limits
Section titled “Rate limits”600 requests/minute per vendor, burst 60. 429 carries Retry-After in seconds. Sustained
overrun contacts you before it throttles you.