OOrbit

API reference

Orbit's JSON API - the same endpoints the dashboard's contact, pipeline, and settings pages call. All request/response bodies are JSON unless noted.

Authentication

Every /api/* endpoint below requires a valid session cookie, set by logging in through the web app (POST /login) or completing signup (POST /signup) / install (POST /install). There is no separate API token today - the JSON API is meant for this app's own dashboard, not third-party integration, though every endpoint here is plain JSON over HTTP and easy to script against once you hold a session cookie. Every endpoint is also workspace-scoped: a contact, deal, or task ID that belongs to another workspace returns 404, never another tenant's data.

Contacts

POST/api/contacts/:id/status

Updates a contact's status (e.g. new, qualifying, qualified, lost), logs a status_change activity, and recalculates the contact's lead score.

Request
{ "value": "new" | "qualifying" | "qualified" | "lost" }
Response
{ "ok": true }
Errors

404 if the contact doesn't exist in your workspace.

POST/api/contacts/:id/suggest

Returns a deterministic, explainable next-best-action suggestion for this contact, based on its stage, open tasks, and time since last activity. Not metered (no model call - rule-based).

Request
(none)
Response
{ "ok": true, "suggestion": "..." }
Errors

404 if the contact doesn't exist in your workspace.

POST/api/contacts/:id/followup

Drafts a follow-up email grounded in the contact's actual open deal and most recent activity, using the workspace's configured AI engine (or the built-in mock engine if none is set). This is a metered AI action.

Request
(none)
Response
{ "ok": true, "text": "...drafted email body...", "engine": "mock" | "gemini" | "claude" }
Errors

404 if the contact doesn't exist in your workspace. 402 (`{ ok:false, upgradeRequired:true }`) if the workspace is at its plan's monthly AI-action limit. 500 if the draft could not be generated.

POST/api/contacts/:id/notes

Logs a note on the contact's activity timeline. A short note (≤200 characters) is logged as-is with no AI and no metering. A longer note (e.g. pasted call/meeting notes) is summarized by AI into a clean recap first - that summarization is the metered AI action. Either way, the contact's lead score is recalculated afterward.

Request
{ "text": "note text, or raw pasted call/meeting notes" }
Response
{ "ok": true }
Errors

400 if text is missing/empty. 404 if the contact doesn't exist in your workspace. 402 (`{ ok:false, upgradeRequired:true }`) if the text is over 200 characters and the workspace is at its plan's monthly AI-action limit. 500 if summarization fails.

Deals

POST/api/deals/:id/stage

Moves a deal to a new pipeline stage, logs a status_change activity on its linked contact (if any), and recalculates that contact's lead score.

Request
{ "stage": "lead" | "qualified" | "proposal" | "negotiation" | "won" | "lost" }
Response
{ "ok": true }
Errors

404 if the deal doesn't exist in your workspace. 400 if stage is not one of the valid pipeline stages.

Tasks

POST/api/tasks/:id/toggle

Marks a task done or not-done.

Request
{ "done": true | false }
Response
{ "ok": true }

Integrations

These endpoints are how the other products in the suite (Rivo, Bloom, Nudge, Relay) - or any external system - talk to Orbit. They are NOT session-authenticated: instead, pass an API key generated in Settings → Integrations as an Authorization: Bearer <key> header. Orbit also publishes events (currently lead.created and deal.won) to any webhook subscription registered for them in Settings → Integrations, signed the same way Stripe signs its webhooks: an X-Signal-Signature: t=<unix timestamp>,v1=<hex hmac> header, where the hmac is SHA-256 of "{timestamp}.{raw JSON body}" keyed with the subscription's own secret (shown once, at creation).

POST/integrations/v1/leads

Creates a contact in this workspace from an external system - a website form, or another product in the suite (e.g. a WhatsApp conversation in Rivo that should become a CRM lead). Logs a note activity and publishes lead.created to any subscribed webhook.

Request
{ "name": "required", "email": "", "phone": "", "source": "integration", "tags": "" }
Response
{ "ok": true, "contact": { "id": "...", "name": "...", "phone": "...", "status": "new", ... } }
Errors

401 if the Authorization header is missing or the API key is invalid. 400 if name is missing/empty.

POST/integrations/v1/activities

Logs an activity onto a contact, looked up by contactId OR by phone number. Phone is the realistic join key when the caller is a different product that has no idea what Orbit's internal contact IDs are - this is how a WhatsApp reply (from Rivo) gets logged back onto the right CRM contact. Phone matching compares digits only, so pass the same phone string the contact was created with for a reliable match.

Request
{ "text": "required", "contactId": "", "phone": "", "type": "note", "engine": "" }
Response
{ "ok": true, "activity": { ... }, "contactId": "...", "contactName": "..." }
Errors

401 if the Authorization header is missing or the API key is invalid. 400 if text is missing/empty. 404 if no contact matches contactId or phone.

Billing

POST/billing/checkout

Starts a subscription upgrade/downgrade. Redirects (302) to a real Stripe Checkout page when Stripe is configured (STRIPE_SECRET_KEY + a price ID for the plan); otherwise falls back to updating the stored plan directly ("demo mode") and redirects to Settings. Session-authenticated, form-encoded (not JSON) since it's posted by a real HTML form.

Request
plan=starter | growth | scale (form-encoded)
Response
302 redirect (to Stripe Checkout, or back to Settings in demo mode).
Errors

400 if plan is missing/invalid. On a configured-but-misconfigured plan (no price ID set for it), redirects back to Settings with a billing_error query param instead of a hard error.

GET/billing/success

Stripe redirects the browser here after a successful Checkout. Does a best-effort immediate sync of the workspace's plan/subscription status from Stripe so the UI updates right away, then redirects to Settings - the webhook below remains the source of truth even if this sync fails.

Request
(none - session_id is a query parameter set by Stripe)
Response
302 redirect to Settings.
GET/billing/portal

Redirects to a real Stripe Billing Portal session for the current workspace (requires Stripe to be configured and the workspace to already have a Stripe customer).

Request
(none)
Response
302 redirect to Stripe, or back to Settings with an error.
POST/webhooks/stripe

Stripe calls this directly - not session-authenticated, verified instead via the Stripe-Signature header (HMAC-SHA256, requires STRIPE_WEBHOOK_SECRET). Handles checkout.session.completed, customer.subscription.updated/created, and customer.subscription.deleted to keep each workspace's plan/subscription_status/current_period_end in sync.

Request
Raw Stripe event JSON (sent by Stripe, not by you).
Response
{ "received": true } on success, or { "error": "..." } with 400 on a bad/missing signature.