Docs

Onboarding sessions API

Public unauthenticated wire format that lets a coding agent open a session, stream onboarding events, present a live dashboard URL, and convert into a claimed Iqrar org.

The /onboarding/* endpoints are the public, unauthenticated surface a coding agent uses to walk a developer through Iqrar setup. They exist so a developer can paste , watch their session populate live, and only authenticate at the moment they want to convert their session into a real Iqrar org.

This page is the wire format. The same endpoints power the prompt's behaviour, but you can call them directly from any HTTP client.

Trust model

  • The agent's side is write-only and unauthenticated. Anyone can open a session.
  • The viewer URL embedded in view_url is read-only and signed. It carries an opaque token; presenting the token reads the session. Sharing the URL shares read access — treat it like a Google Doc share link.
  • Session events are tagged source: "onboarding" in the audit chain. They are operator-asserted, not agent-signed — no Ed25519 identity exists yet at this stage.
  • A session that's never claimed expires after 30 days. Events written to it are deleted at expiry.
  • A session converts into an Iqrar org via the magic-link claim flow. After claim, the session events stay attached to the session — they're operator-asserted, not agent-bound, so they don't migrate into the per-agent audit chain. The session view remains accessible at view_url for the lifetime of the org as a record of how it onboarded.

POST /onboarding/sessions

Open a new onboarding session.

Request

POST /onboarding/sessions
Content-Type: application/json

{
  "user_agent": "claude-code/0.5.0",
  "project_hint": "github.com/acme/agents"
}

Both fields are optional. project_hint is a free-form string the coding agent can use to tag the session — typically the git remote URL. The server doesn't act on it; it's surfaced in the dashboard so the developer can find their session.

Response

200 OK
Content-Type: application/json

{
  "session_id": "ses_01HRX9...",
  "view_url": "https://iqrar.io/onboarding/ses_01HRX9...?t=...",
  "expires_at": 1749146291000
}

view_url is what you hand back to the developer. They open it in a browser to watch the chain populate. The token in the query string is single-use-per-fetch and rotates on every refresh.

GET /onboarding/lookup?domain=...

Check whether an Iqrar org already exists for a given email domain. Privacy-aware — does not enumerate; rate-limited; returns an opaque response.

Request

GET /onboarding/lookup?domain=acme.com

Response

200 OK
Content-Type: application/json

{
  "claimed": true,
  "claim_hint": "magic-link sent to the existing admin"
}

When claimed is true, the coding agent should prompt the developer to claim via the existing admin's email rather than re-registering. When claimed is false, registration proceeds normally.

The endpoint returns 200 for every well-formed query regardless of result. There is no 404.

POST /onboarding/sessions/:session_id/events

Append events to a session. The agent calls this throughout the prompt — once for each Q&A answer, once for the repo-scan report, once after SDK install, etc.

Request

POST /onboarding/sessions/ses_01HRX9.../events
Content-Type: application/json

{
  "events": [
    {
      "type": "onboarding.jurisdiction_selected",
      "ts": 1746478291000,
      "payload": { "jurisdiction": "AE" }
    },
    {
      "type": "onboarding.capabilities_inferred",
      "ts": 1746478295000,
      "payload": {
        "input": "customer support chat for our SaaS",
        "capabilities": ["consumer_chatbot"],
        "inferred_tier": "limited"
      }
    }
  ]
}

Response

202 Accepted
Content-Type: application/json

{ "accepted": 2 }

Events are durably enqueued. The viewer at view_url polls for changes and renders new events within ~1s.

Canonical event types

The viewer renders these specially. Custom onboarding.* types are accepted and shown as raw JSON.

TypeWhen to emitRequired payload
onboarding.session_openedOnce, on session creation. The server emits this automatically.{ user_agent?, project_hint? }
onboarding.jurisdiction_selectedAfter Q1.{ jurisdiction: "AE" }
onboarding.capabilities_inferredAfter Q2.{ input: string, capabilities: string[], inferred_tier: "minimal"|"limited"|"high"|"critical" }
onboarding.repo_scannedAfter the agent scans the repo and the developer authorises sending the report.{ frameworks: string[], agents: Array<{ path, framework, model?, capabilities, tier }> }
onboarding.sdk_installedAfter SDK install + wrap is complete.{ language: "ts"|"py", agent_count: number }
onboarding.first_telemetryWhen the dev server fires its first wrapped invocation. Emitted by the SDK in IQRAR_ENV=dev mode against this session, if linked.{ agent_id: string }

GET /onboarding/sessions/:session_id

The endpoint the viewer page hits. Returns session metadata and the event chain. Requires the token from view_url.

Request

GET /onboarding/sessions/ses_01HRX9...?t=<token>

Response

200 OK
Content-Type: application/json

{
  "session_id": "ses_01HRX9...",
  "opened_at": 1746478290000,
  "expires_at": 1749146291000,
  "claimed": false,
  "events": [
    { "type": "onboarding.session_opened", "ts": 1746478290000, "payload": {...} },
    { "type": "onboarding.jurisdiction_selected", "ts": 1746478291000, "payload": {...} }
  ]
}

Polling cadence: the viewer page polls every 1s while the tab is visible, every 30s when hidden.

POST /onboarding/sessions/:session_id/claim

Convert an unauthenticated session into a real Iqrar org. The flow is two-step: request a claim, then confirm via magic link.

Step 1 — request

POST /onboarding/sessions/ses_01HRX9.../claim
Content-Type: application/json

{
  "email": "leonard@acme.com",
  "org_slug": "acme"
}
202 Accepted
{
  "claim_id": "clm_01HRX9...",
  "magic_link_sent_to": "leonard@acme.com",
  "delivery": "email"
}

A one-time magic link is sent to the email via Resend. The link's destination is /onboarding/claim/:claim_id?t=<token> on the web app.

Dev fallback. When the API isn't configured with a RESEND_API_KEY, the link is not delivered by email. The response is augmented with the link inline so local development still works:

202 Accepted
{
  "claim_id": "clm_01HRX9...",
  "magic_link_sent_to": "leonard@acme.com",
  "delivery": "fallback",
  "delivery_reason": "not_configured",
  "magic_link_preview": "https://iqrar.io/onboarding/claim/clm_01HRX9...?t=..."
}

delivery_reason is "not_configured" when the mailer isn't set up, or "send_failed" when Resend rejected the request — in both cases magic_link_preview is available as a recovery path. Production deployments configured with Resend should never see the fallback shape.

Step 2a — preview (idempotent, link-safe)

The magic link itself points at a web page on iqrar.io that loads claim metadata before any mutation. The page hits:

GET /onboarding/claim/clm_01HRX9...?t=<token>

200 OK
{
  "claim_id": "clm_01HRX9...",
  "session_id": "ses_01HRX9...",
  "email": "leonard@acme.com",
  "org_slug": "acme",
  "expires_at": 1746480091000,
  "expired": false,
  "confirmed": false
}

This endpoint never mutates state, so email previewers and link scanners that prefetch the URL cannot consume the claim. The web page renders a "Confirm and reveal API key" button.

Step 2b — confirm (mutating, requires user click)

POST /onboarding/claim/clm_01HRX9...?t=<token>

200 OK
{
  "ok": true,
  "org": "acme",
  "session_id": "ses_01HRX9...",
  "api_key": "iqr_a1b2c3...",
  "api_key_id": "key_01HRX9...",
  "api_key_prefix": "iqr_a1b2"
}

The POST:

  1. Verifies the token.
  2. Creates the orgs.acme row if not already claimed.
  3. Issues an API key, storing only its SHA-256 hash. Plaintext is returned exactly once in this response.
  4. Marks the session claimed; binds the email's domain to the org for future /onboarding/lookup calls.
  5. Appends onboarding.claimed to the session event chain. The session itself is preserved — view_url continues to work as a record of how the org was onboarded.

Subsequent POSTs return 409 already_confirmed. The plaintext API key is never recoverable from the database — if lost, the developer must rotate via the dashboard.

After successful claim, the session view at view_url flips to a "claimed by acme" banner.

Errors

All endpoints return JSON errors with shape { error: string, code: string }.

CodeMeaning
session_not_foundThe session ID doesn't exist or has expired.
session_expiredThe session passed its 30-day expiry.
token_invalidThe viewer or claim token is wrong, expired, or revoked.
domain_already_claimedThe email domain in a claim request already has an Iqrar org. The response includes claim_hint for the existing-admin recovery flow.
rate_limitedToo many requests from the same IP. The lookup and claim endpoints have stricter limits than events.
event_too_largeA single event payload exceeded 64 KB. Truncate or summarise before retrying.

Once you're claimed

The onboarding endpoints stop being relevant after claim. The agent moves to the authenticated /register and /telemetry endpoints documented in the , using the IQRAR_API_KEY issued at claim time.

© 2026 Cortex Innovations (Pty) Ltd. Iqrar is a working name pending trademark clearance.Powered by Stratafy