Skip to main content

Orchestrate the public API surfaces

Orbit ships a family of unauthenticated endpoints under /api/v1/public/* that exist for one reason: your customer’s browser (or your customer’s customer) can drive a self-service flow — check a callback, sign a consent form, answer a survey, search your help center — without you proxying every click through your backend. This guide is the orchestration map. It covers the queue-callback lifecycle end to end, then runs the same pattern across consent, surveys/CSAT, help-center search, and the .well-known discovery documents external agents resolve against. The last two sections explain the signed-token model these endpoints share, and which org-scoped endpoints must never leave your server. Every path below is relative to https://api.orbit.devotel.io/api/v1.
Public endpoints are rate-limited per IP and intentionally unauthenticated (security: [] in the OpenAPI spec). They carry no API key, no session, and no tenant plugin. Never send a live secret key (dv_live_sk_…) from a browser to “protect” one — you leak the key.

When public surfaces matter

The public surface exists wherever your end user (a caller, a recipient, a visitor, an external agent) needs to act directly: Pick the token-scoped surface when the grant is a single resource (one callback, one survey link, one conversation rating). Pick the tenant-scoped surface when the configuration itself is published (consent config, queue booking window).

Public-callback flow (worked sequence)

This is the canonical orchestration pattern: your server creates the resource, the public token carries the grant, the customer’s browser drives the lifecycle.
  1. Create the callback server-side. Your backend (with a real API key) requests a callback — either via the queue endpoint POST /api/v1/voice/callbacks (or the disposition-callback rule on the queue), or the virtual-hold callback-in-queue flow. The creation response returns the callback id.
  2. Sign a public token and hand it to the customer. The platform signs a compact HMAC token that embeds the callback id and the tenant it belongs to (the tenant schema travels inside the signed payload — it is not a query param). In the default flow the dispatch notification itself texts the caller a link like https://api.orbit.devotel.io/api/v1/public/callbacks/<token>. If you build your own “call me back” UI, generate the link the same way and hand the token to the browser.
  3. The customer reads status. From the browser:
    Returns the current state (pending, dialing, terminal). Poll it to keep the “we’ll call you back at …” card up to date.
  4. The customer cancels or reschedules. Same token, same no-auth model:
    Both mutations are guarded: a callback already dialing or in a terminal state is never altered, and a re-tap of cancel returns “already canceled” instead of an error. A 404 (CALLBACK_NOT_FOUND) means the row is gone or the token doesn’t validate — render a friendly expired-link page, not a retry loop.
  5. Render the landing page if you don’t want to build UI. GET /public/callbacks/{token} (without a suffix) returns a self-contained HTML status page with cancel/reschedule buttons — suitable as the direct tap target of the SMS link.
Do not recreate the callback with a fresh POST when the customer reschedules — reschedule in place. Creating a second pending callback for the same number double-dials them.

The consent gate has two calls and one rule: opt-in needs proof of ownership; opt-out never does.
  1. Fetch the tenant’s published form config so your page renders the tenant’s own branding, copy, and allowed channels:
    Unknown or disabled tenants return enabled: false with empty branding — the endpoint cannot be used to enumerate which tenant ids exist. Treat enabled: false as “render your own fallback form,” not as an error to surface.
  2. Post the consent decision:
    granted: false (opt-out) is one-click and needs no proof token. granted: true (opt-in) is only recorded as opted_in when the signed proof token delivered to the recipient’s own address (email or phone) validates — this is what stops someone from opting in a number they don’t control. The response lists one consent-record id per channel written.
Full hosted-page walkthrough: Public consent form.

Surveys and CSAT

Both are token-scoped: the link itself is the grant. Survey (NPS/CSAT) links — the score row (0–10) plus optional comment, idempotent per link:
A repeat submission never double-counts — the endpoint acknowledges with data.ok: true. An invalid or expired token returns 403 on submit (404 HTML on the page GET). Conversation rating (CSAT 1–5 / NPS 0–10) — same token model against one conversation:
The scale is bound into the signed token, and the first rating wins — a re-tap returns the stored score with data.already_submitted: true instead of overwriting. Administrative setup (scorecards, auto-survey rules) still happens in the dashboard or the org-scoped API — see Post-call surveys. The public endpoints here are only the recipient-facing capture step.
Point your help widget’s search box at the public search endpoint instead of standing up your own index:
Title matches rank ahead of body matches; results return slug, title, summary, updated_at, and view_count. The org query parameter selects the published help center — it scopes to public, published articles only, so no unpublished draft leaks. From the article page you can route a “was this helpful?” vote to POST /public/help/feedback (visitor IP is hashed, never stored raw) and escalate to POST /public/help/tickets when self-serve fails. Set up the help center itself in Brand and launch your hosted help center.

Discovery surface: .well-known for external agents

External-agent handoffs resolve against public, edge-cached discovery documents — no shared secret, no onboarding:
  • GET /public/.well-known/jwks.json — the EdDSA (Ed25519) JWK Set that verifies Orbit’s A2A agent identity tokens and signed AgentCards. Public key material only; an empty keys array means signing isn’t provisioned yet.
  • GET /public/.well-known/ans/registry/lookup — Agent Trust Registry lookup: pass ?org=<slug> plus one of ans URI, agentId, or E.164 phone to get the tenant’s brand-identity verification status, STIR/SHAKEN attestation tiers, and the agent’s scoped capability surface. Unknown orgs and disabled lookups collapse to the same 404.
  • GET /public/mcp/.well-known/mcp.json — the hosted MCP server manifest (Streamable-HTTP transport at /api/v1/mcp) so MCP-aware agent hosts auto-configure without a hand-shared URL.
  • GET /public/.well-known/http-message-signatures-directory — the public-key directory for verifying HTTP Message Signatures on Orbit-signed outbound requests.
Cache these aggressively (they are edge-cached anyway), tolerate empty key sets, and re-fetch on a verification failure before you reject a token.

Token model and scoping guarantees

All token-scoped public endpoints share one construction, and it is worth holding deliberately:
  • The token is the whole grant. It is base64url(payload).base64url(HMAC-SHA256(secret, payload)). The payload carries the resource id and the tenant it belongs to, so a browser cannot reach across tenants by editing the URL — any tamper fails a constant-time HMAC compare. There is no session, no cookie, no tenant plugin.
  • Hold the token in the URL, nowhere else. Deliver it in the SMS/email link (or in the DOM of your served page) and let the customer carry it back. Do not log it, do not mirror it into your analytics, and do not persist it in your own database — you already have the callback id server-side. If the link leaks, the blast radius is one resource’s lifecycle (cancel/reschedule), not org data.
  • Expired or forged token → 404/403, never a data leak. A bad token on the callback surface returns CALLBACK_NOT_FOUND (404); survey submit returns 403. Generate the link, hand it over, then forget it.
  • Scoping is per-link. One token maps to one callback, one survey invitation, one conversation rating. A customer holding a valid token can act on that resource only — never on the org’s list of callbacks, other customers, or queue configuration.
The one operations rule: tokens expire (callback links live a week; survey links follow the campaign’s TTL). Poll status, surface already_submitted / already_canceled responses as success, and re-issue the grant server-side when you need a longer window — never extend it client-side.

What NOT to call from the browser

The public surface above is the complete list of endpoints your customer’s browser may call. Everything else on /api/v1 is org-scoped and expects an X-API-Key header:
  • Never put a dv_live_sk_* key in browser code. Anyone with devtools can read it and drain your wallet. Server-only.
  • Org-scoped siblings of the same resource stay server-side. GET/DELETE /voice/callbacks/{id}, PATCH /voice/callbacks/{id}, queue configuration, survey definitions, consent-management settings — these leak or mutate org data. The public token endpoints exist precisely so the browser does not need the org-scoped route; don’t “simplify” by exposing the keyed one.
  • Don’t proxy-re-log the public endpoint. If you must mediate (e.g. embedding the status check in a logged widget), call the public endpoint from the browser directly — bouncing it through your backend adds latency and gains nothing, because the token is already the ceiling of access.
If a flow you want to expose isn’t in the /public/* list, file it as a gap — don’t work around it by shipping a key.

FAQ

Are public endpoints rate-limited? Yes — every public endpoint is IP-rate-limited (the same RATE_LIMIT_PUBLIC_UNAUTH class as the other public landing pages). Cache .well-known documents, poll callback status on a slow cadence (once per second at most), and treat a 429 as a signal to back off, not to retry harder. What if a customer loses the link? Re-issue server-side: your backend still knows the callback id and can mint a new token the same way the dispatch notification did. Can I brand the HTML landing pages? The callback landing page renders the queue/tenant branding you configured for the public-booking surface; consent config carries its own branding block. Anything beyond that is your own hosted page hitting the JSON endpoints.