# Server API

> The HTTPS surface your backend uses to reach Sollar — request signing, endpoints, webhooks and the encrypted-room constraint.

Source: https://miniapp.sollar.com/reference/api/server/

---

Plane ③ of the [integration contract](/reference/api/): 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

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

```
base       = "v0:" + timestamp + ":" + raw_body
signature  = "v0=" + hex(HMAC_SHA256(signing_secret, base))
```

```http
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
```

```js

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.

> **DANGER**
The signing secret lives in a secret manager, never in a file, a repository, an environment file, or
a CI variable. Rotate it from the developer console; both the old and new secret verify during a
one-hour overlap so rotation needs no downtime.

## Endpoints

### Post a card into a room

```http
POST /v1/rooms/{room_id}/cards
```

```json
{
  "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

```http
POST /v1/users/{app_user_id}/notifications
```

```json
{
  "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

```http
GET /v1/apps/{mini_app_id}/installations/{app_user_id}
```

```json
{
  "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

```http
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.

```json
{
  "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

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. |

```json
{
  "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

> **CAUTION**
**Your backend cannot post into an end-to-end encrypted room, and no endpoint here changes that.**

Sollar's servers do not hold the room keys. Your backend is not a cryptographic member of the room.
This is the property that makes the encryption meaningful, so there is no server-side path around
it — not a special scope, not an enterprise tier, not a support request.

In the Enterprise and Sovereign tiers, encryption is on by default. Assume the room is encrypted.

Two paths that work:

1. **`sollar.message.send()`** — the message leaves from the user's own client, under their identity.
   The user confirms it.
2. **An AI agent that is a cross-signed member of the room** posts under its own identity. It sees
   the room's content exactly as an invited human member would — visible and auditable, not
   confidential during processing.

Design the notification path before building the feature.

## Errors

RFC 9457 problem details:

```json
{
  "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

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