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
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_urlis 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_urlfor 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.
| Type | When to emit | Required payload |
|---|---|---|
onboarding.session_opened | Once, on session creation. The server emits this automatically. | { user_agent?, project_hint? } |
onboarding.jurisdiction_selected | After Q1. | { jurisdiction: "AE" } |
onboarding.capabilities_inferred | After Q2. | { input: string, capabilities: string[], inferred_tier: "minimal"|"limited"|"high"|"critical" } |
onboarding.repo_scanned | After the agent scans the repo and the developer authorises sending the report. | { frameworks: string[], agents: Array<{ path, framework, model?, capabilities, tier }> } |
onboarding.sdk_installed | After SDK install + wrap is complete. | { language: "ts"|"py", agent_count: number } |
onboarding.first_telemetry | When 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:
- Verifies the token.
- Creates the
orgs.acmerow if not already claimed. - Issues an API key, storing only its SHA-256 hash. Plaintext is returned exactly once in this response.
- Marks the session claimed; binds the email's domain to the org for future
/onboarding/lookupcalls. - Appends
onboarding.claimedto the session event chain. The session itself is preserved —view_urlcontinues 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 }.
| Code | Meaning |
|---|---|
session_not_found | The session ID doesn't exist or has expired. |
session_expired | The session passed its 30-day expiry. |
token_invalid | The viewer or claim token is wrong, expired, or revoked. |
domain_already_claimed | The email domain in a claim request already has an Iqrar org. The response includes claim_hint for the existing-admin recovery flow. |
rate_limited | Too many requests from the same IP. The lookup and claim endpoints have stricter limits than events. |
event_too_large | A 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 IQRAR_API_KEY issued at claim time.