LIVE · AUDIT-CHAINED · EU-RESIDENT
SYSTEM · 99.99% UPTIME
v 1.0 ↗ MADE IN EU

Sessions API

A Session is one inspection. Create one, mint a field-user invite, capture evidence, end it. Everything else hangs off this resource.

The Session object

{
  "id": "0c8f4d2e-1a3b-4c5d-9e7f-1234567890ab",
  "status": "open",
  "operator_email": "ops@yourco.com",
  "scheduled_for": "2026-05-23T10:00:00Z",
  "started_at": "2026-05-23T10:00:14Z",
  "ended_at": null,
  "notes": "Vehicle damage — claim CL-2026-0042",
  "locale": "fr",
  "consent_state": { "camera": "granted", "gps": "granted" },
  "campaign": null,
  "created_at": "2026-05-21T14:21:00Z",
  "updated_at": "2026-05-23T10:00:14Z"
}
StatusMeaning
createdSession row exists; nobody has joined yet.
openField user has joined; session is live.
recordingRecording in progress (optional, operator-triggered).
closedSession ended. Chain head anchored at TSA; reports available.
expiredScheduled session that nobody joined within the TTL.

Create a session

POST /api/v1/public/sessions — scope sessions:write

curl -X POST https://app.nexbasira.com/api/v1/public/sessions \
  -H "Authorization: Bearer nb_sec_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "notes": "Vehicle damage — claim CL-2026-0042",
    "scheduled_for": "2026-05-23T10:00:00Z",
    "locale": "fr"
  }'

Returns the freshly-created Session object (HTTP 201).

Body fields

FieldTypeRequiredNotes
notesstringnoVisible to the operator. Shown in invite emails.
scheduled_forISO 8601noFuture date triggers reminder emails 24h + 1h before. Omit for "start now".
localestringnoOne of the 14 supported locales. Drives the SPA + PDF report language. Defaults to the org's preference.
campaignUUIDnoOptional FK to a Campaign for batched reporting.

List sessions

GET /api/v1/public/sessions — scope sessions:read

curl https://app.nexbasira.com/api/v1/public/sessions?limit=25 \
  -H "Authorization: Bearer nb_sec_..."

Cursor-paginated. Pass cursor from the previous response's next_cursor to page.

{
  "data": [{ /* Session, Session, ... */ }],
  "has_more": true,
  "next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAy..."
}

Retrieve a session

GET /api/v1/public/sessions/{session_id} — scope sessions:read

End a session

POST /api/v1/public/sessions/{session_id}/end — scope sessions:write

Closes the session, triggers the chain-head anchor at all configured TSAs, and kicks off recording post-processing if a recording was running. Idempotent — calling on an already-closed session returns the closed Session object without re-anchoring.

Mint a field-user invite

POST /api/v1/public/sessions/{session_id}/participants — scope participants:write

curl -X POST https://app.nexbasira.com/api/v1/public/sessions/0c8f.../participants \
  -H "Authorization: Bearer nb_sec_..." \
  -H "Content-Type: application/json" \
  -d '{
    "recipient_first_name": "Alex",
    "recipient_last_name": "Garcia",
    "recipient_email": "alex@policyholder.com",
    "send_email": true,
    "ttl_minutes": 1440
  }'
{
  "id": "i-1",
  "session": "0c8f...",
  "role": "field",
  "expires_at": "2026-05-24T10:00:00Z",
  "recipient_first_name": "Alex",
  "recipient_last_name": "Garcia",
  "recipient_email": "alex@policyholder.com",
  "recipient_phone": "",
  "join_url": "https://app.nexbasira.com/join/0c8f.../?t=tok_PLAINTEXT_ONCE",
  "otp_code": "487192",
  "otp_required": true,
  "otp_expires_at": "2026-05-23T10:10:00Z",
  "created_at": "2026-05-23T10:00:00Z"
}

The join_url and otp_code are shown exactly once in the response. Tokens are IP/UA-pinned, single-use, and time-boxed.

Two-factor join (OTP)

When you supply at least one of recipient_email or recipient_phone, the platform auto-mints a 6-digit numeric OTP and sends it on the matching channel(s) in a separate message from the join URL — defence-in-depth so a forwarded email or SMS doesn't leak both factors at once. The plain code is also returned in the response (otp_code) so you can re-share it manually if delivery fails.

Field-side, the SPA pages the OTP prompt on first redemption. Submit the code via the X-Join-OTP header on a retry of GET /v1/sessions/{id}/join/{token} — the header (not URL) keeps the code out of browser history and access logs.

OTP rules:

  • 6-digit numeric, hashed with SHA-256 + pepper at rest.
  • 10-minute TTL from issue.
  • 5 wrong attempts locks the invite (HTTP 423) — operator must reissue.
  • Verified once at first redemption; re-redemption from the same IP / UA pair skips the gate (the token is already pinned).
  • Face-to-face URL handoff (no recipient_email + no recipient_phone) skips OTP minting — the URL alone is the auth factor. Use this for in-person handoffs only.

Response codes on GET /v1/sessions/{id}/join/{token}:

StatusBodyMeaning
200{field_session_token, livekit, ...}OTP passed (or not required); field session is live.
401{detail:"otp_required", channels:[...], channel_hint_email, channel_hint_phone}SPA should render the OTP entry form.
401{detail:"otp_invalid", attempts_remaining}Wrong code; show remaining attempts.
401{detail:"otp_expired"}10-min window elapsed; operator must reissue.
423{detail:"otp_locked"}5 wrong attempts; invite is dead until reissue.

Common errors

StatusCodeWhen
402billing.subscription_past_dueOrg's Stripe subscription is past due.
402billing.free_plan_minutes_exhaustedFree / Pilot tier has used its 5 inspections.
403permission_deniedCredential lacks sessions:write scope.
409session.already_endedTrying to end a session that's already closed (rare — `end` is idempotent normally).
429rate_limited60-rpm per-credential or 600-rpm per-org throttle hit. See X-RateLimit-Reset header.

See Errors + rate limits for the full error envelope shape and retry guidance.