Skip to content

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.

Every request is signed with HMAC-SHA256 over the raw body.

base = "v0:" + timestamp + ":" + raw_body
signature = "v0=" + hex(HMAC_SHA256(signing_secret, base))
POST /v1/rooms/!abc:sollar.com/cards HTTP/1.1
Host: api.sollar.com
Content-Type: application/json
X-Sollar-Request-Timestamp: 1788278400
X-Sollar-Signature: v0=8f2c...
X-Sollar-Mini-App: com.acme.erp
import { 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:

  1. Sign the raw body, before any JSON parsing. A re-serialised body produces a different signature and you will chase it for a day.
  2. Constant-time comparison. === on a signature leaks its bytes through timing.
  3. A short timestamp window — five minutes. Without it a captured request replays forever.
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.

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.

POST /v1/agents/verify

Given 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" }
}

Sollar calls your backend for events you subscribe to in the developer console. The signature scheme is identical, and the same three rules apply.

EventSent when
installation.createdA user installs the mini app
installation.deletedUninstalled, or removed by an administrator
scope.granted / scope.revokedA permission changes
user.deprovisionedThe user leaves the tenant — delete their data
app.revokedSollar 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.

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"
}
StatusMeaning
400Malformed request; detail names the field
401Signature invalid, missing, or outside the timestamp window
403Scope not granted, or tenant policy forbids it
404Unknown room, user or mini app
409State conflict — encrypted room, version revoked
429Rate limited; honour Retry-After
503Retriable

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.

600 requests/minute per vendor, burst 60. 429 carries Retry-After in seconds. Sustained overrun contacts you before it throttles you.