SymorvaDocs
SDK & APIs

Server API & webhooks

The preview operator API of the community server — authentication, conventions, resources — and the signed webhook events modules emit.

Developer preview· Interface published, subject to change

This page describes the preview interface of the Server API and webhooks. Paths, fields and event names may change before general availability.

The Server API lets operators integrate their community with their own systems: manage members and approvals, schedule rooms, curate the directory, read usage, and receive events. It is served by the community server in the operator's deployment, so requests and data never leave the operator's environment.

Conventions

Base URL. https://<deployment.domain>/api/v1. Examples below use https://community.orbit.example/api/v1.

Authentication. Operator API keys, created in the admin console and sent as a bearer token. Each key has scopes — for example members:read, members:write, rooms:write, directory:write, usage:read — and is tied to the console user who created it for auditing.

curl https://community.orbit.example/api/v1/community \
  -H "Authorization: Bearer $SYMORVA_API_KEY"

Idempotency. Every POST accepts an Idempotency-Key header. Repeating a request with the same key and body returns the original result; the same key with a different body is rejected with idempotency_conflict.

Pagination. List endpoints take limit (1 to 100, default 20) and cursor, and return nextCursor until the last page.

Errors. Non-2xx responses share one envelope:

{
  "error": {
    "code": "forbidden",
    "message": "The API key lacks the members:write scope.",
    "requestId": "req_01JBX7Q4M9W8ZK2N5T3F6RVY0D"
  }
}
StatusCodes
400invalid_request, idempotency_conflict
401unauthenticated
403forbidden, module_disabled
404not_found
409conflict, stale_revision
429rate_limited — see the Retry-After header
5xxinternal, unavailable

Concurrency. Resources edited by more than one operator carry a revision. Updates must send the revision they read; a stale revision returns 409 stale_revision instead of overwriting someone else's change.

Resources

Method and pathPurposeModule
GET /communityCommunity identity, enabled modules and optionscore
GET /membersList members; filter by role or statuschat
GET /members/{memberId}Read a memberchat
PATCH /members/{memberId}Change a member's rolechat
POST /members/{memberId}/restrictionsMute, suspend or ban a memberchat
GET /join-requestsPending join requestschat
POST /join-requests/{requestId}/approveApprove a join requestchat
POST /join-requests/{requestId}/rejectReject a join requestchat
GET /groupsList groupschat
POST /groupsCreate a groupchat
POST /announcementsSend a system announcementchat
GET /roomsList scheduled and live roomsmeetings
POST /roomsSchedule a roommeetings
POST /rooms/{roomId}/endEnd a live roommeetings
GET /rooms/{roomId}/recordingsList a room's recordingsmeetings
GET /ai/grantsGrant metadata for audit — never contentaiStudio
GET /wallet/transfersList transferswallet
GET /wallet/transfers/{transferId}Read a transferwallet
GET /directory/listingsList directory listingsdapps
POST /directory/listingsCreate a listingdapps
PATCH /directory/listings/{listingId}Update a listingdapps
GET /usageUsage records by unit and time windowmetering
GET /webhook-endpointsList webhook endpointscore
POST /webhook-endpointsRegister an endpointcore
DELETE /webhook-endpoints/{endpointId}Remove an endpointcore

Endpoints of a disabled module return 403 module_disabled.

There is deliberately no endpoint that reads message content, creates an AI grant on a member's behalf, or moves a member's funds. Announcements are system messages: they originate on the server, are not end-to-end encrypted, and are labelled as system messages in apps.

Example: schedule a room

curl -X POST https://community.orbit.example/api/v1/rooms \
  -H "Authorization: Bearer $SYMORVA_API_KEY" \
  -H "Idempotency-Key: 6f1c2a3e-governance-2026-11-04" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Governance call",
    "hostId": "mem_8Q2K",
    "startsAt": "2026-11-04T10:00:00Z",
    "durationMinutes": 60,
    "broadcast": { "enabled": true, "maxAudience": 2000 }
  }'
{
  "id": "room_3VJ9",
  "title": "Governance call",
  "status": "scheduled",
  "mode": "large-room",
  "hostId": "mem_8Q2K",
  "startsAt": "2026-11-04T10:00:00Z",
  "durationMinutes": 60,
  "broadcast": { "enabled": true, "maxAudience": 2000 },
  "revision": "1"
}

maxAudience cannot exceed the manifest's modules.meetings.broadcast.maxAudience.

Example: read usage

curl "https://community.orbit.example/api/v1/usage?unit=participant-minutes&from=2026-10-01&to=2026-11-01" \
  -H "Authorization: Bearer $SYMORVA_API_KEY"
{
  "data": [
    {
      "recordId": "use_01JBX8",
      "module": "meetings",
      "unit": "participant-minutes",
      "quantity": 1840,
      "windowStart": "2026-10-01T00:00:00Z",
      "windowEnd": "2026-10-02T00:00:00Z"
    }
  ],
  "nextCursor": "c_02"
}

Webhooks

Modules emit events to the endpoints you register. Delivery is at least once: the same event can arrive more than once, and events can arrive out of order. Use the event id to deduplicate and the occurredAt timestamp, not arrival order, to sequence.

Delivery

Each delivery is an HTTPS POST with a JSON body and these headers:

HeaderValue
Symorva-Event-IdThe event's unique id
Symorva-Event-TypeThe event type, for example chat.join_request.created
Symorva-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>

Respond with any 2xx status within 10 seconds to acknowledge. Other responses and timeouts are retried with exponential backoff for up to 24 hours; after that the event is kept in the endpoint's failed-delivery list in the admin console and can be replayed.

Event envelope

Prop

Type

chat.join_request.created
{
  "id": "evt_01JBX9R2C4",
  "type": "chat.join_request.created",
  "community": "orbit-collective",
  "occurredAt": "2026-10-28T09:41:12Z",
  "data": {
    "requestId": "jr_5TQ1",
    "memberId": "mem_2B7X",
    "groupId": "grp_governance"
  }
}

Verifying signatures

The signature is an HMAC-SHA256 over the timestamp, a period and the raw request body, keyed with the endpoint's signing secret. Verify it before parsing the body, and reject timestamps outside a short tolerance window to prevent replay.

verify-webhook.ts
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifySymorvaSignature(
  rawBody: string,
  header: string,
  secret: string,
  toleranceSeconds = 300,
): boolean {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
  const timestamp = Number(parts.t);
  if (!Number.isFinite(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;

  const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest();
  const received = Buffer.from(parts.v1 ?? "", "hex");
  return received.length === expected.length && timingSafeEqual(received, expected);
}

Event catalog

ModuleEvents
Chatchat.member.joined, chat.member.left, chat.join_request.created, chat.join_request.resolved, chat.role.changed, chat.group.created, chat.message.created, chat.report.created
Meetingsmeetings.room.scheduled, meetings.room.started, meetings.room.ended, meetings.participant.joined, meetings.participant.left, meetings.recording.started, meetings.recording.ready
AI studioai.grant.created, ai.grant.revoked, ai.job.created, ai.job.completed, ai.job.failed, ai.asset.published
Walletwallet.transfer.created, wallet.transfer.settled, wallet.transfer.failed, wallet.deposit.confirmed, wallet.withdrawal.requested, wallet.withdrawal.reviewed
DAppsdapps.listing.published, dapps.listing.unpublished, dapps.request.approved, dapps.request.denied
Creator economycreator.tip.sent, creator.remix.attributed, creator.payout.settled

Field lists for each event are on the module pages. Events never contain message content, prompts, generated content, data released to partner apps, or key material. Subscribing an endpoint to an event of a disabled module is rejected.

On this page