Registration

Butt-Dial includes self-service registration. Users create an account, verify their email, and get API credentials — no manual admin setup needed.

Flow

  1. Visit the landing page and click Get Started
  2. Fill out the registration form (name, email, team, password)
  3. Receive a verification email with a 6-digit code
  4. Enter the code to verify
  5. Get a team token and start using the API

Endpoints

EndpointMethodDescription
/auth/loginGETLogin/register page
/auth/registerPOSTCreate account
/auth/verifyGET/POSTEmail verification
/auth/loginPOSTLogin with existing credentials

Email Verification

Workspace Token (`teamToken`)

After verification, every organization gets a single teamToken — the

canonical name for the workspace bearer token. Pass it as

Authorization: Bearer on every authenticated request.

GET /sse?token=<teamToken>&agentId=<agent-id>  (agentId required for team tokens)

Legacy aliases — deprecated, will be removed

The same value is also returned as ownerToken, team_token, and

owner_token in the POST /api/orgs/verify response. **These are

deprecated** — use teamToken. Snake_case aliases across the auth

surface will be removed; the OpenAPI spec marks individual properties

deprecated: true where the rename has shipped.

The migration is in flight across the rest of the auth surface:

Canonical (camelCase)Deprecated (snake_case alias)
teamTokenteam_token, owner_token, ownerToken
orgIdorg_id
agentIdagent_id
displayNamedisplay_name
callbackUrlcallback_url
retryAfterretry_after

Hosts should migrate to camelCase. The snake_case keys still ship for

one release alongside the camelCase canonicals.

> Note (2026-05-08): earlier sprints set response-level

> Deprecation: true and Sunset: headers on every endpoint that

> echoed snake_case aliases. That was misleading — RFC 8594 describes

> the *resource* as deprecated, not specific body keys. Those headers

> have been removed from POST /agents, GET /agents/me,

> GET /agents/{id}, and POST /api/orgs/register; those endpoints

> are NOT being sunset. See "Endpoint deprecations" below for the

> three surfaces that genuinely are.

Endpoint deprecations

The following endpoints are scheduled for removal. Each carries

Deprecation: true, Sunset: , and

Link: ; rel="successor-version", ; rel="deprecation"

on every response — RFC 8594 + draft-ietf-httpapi-deprecation-header

compliant. SDK clients can auto-migrate by following the

successor-version link.

Deprecated endpointSuccessorSunset
POST /api/v1/provisionPOST /api/v1/agents (with channels + dns in the body)2026-07-27
POST /api/v1/onboardPOST /api/v1/agents (with channels + dns in the body)2026-07-27
GET /api/orgs/verify?token=.../verify#token=... (browser-fragment route — token never appears in the URL bar or referer)server-boot + 90 days

Migration notes:

consolidated endpoint accepts the same fields as either legacy

endpoint, plus an optional channels array (replaces capabilities)

and an optional dns block for the email-domain verification step

that /onboard previously bundled. Behavior is identical;

channel-by-channel migration is unnecessary.

route puts the token in the URL fragment so it never crosses the

network in a Referer header or appears in server logs. Email-link

scanners that probe the link no longer burn the token. Old in-flight

emails will continue to work via the legacy GET until the Sunset

date.

If you need a Sunset extension or successor change, contact BD before

the date in the table.

Verification polling — `pollToken`

POST /api/orgs/register returns a pollToken alongside the orgId.

Call GET /api/orgs/:orgId/verification-status?poll_token=

to watch for the user clicking the verification email. The endpoint

requires either the pollToken (pre-verify) or a Bearer teamToken

(post-verify); unauthenticated probes get 401 AUTH_REQUIRED.

Cross-system mapping — `external_org_id`, `external_user_id`

POST /api/orgs/register accepts external_org_id and external_user_id

(both opaque strings, ≤128 chars). They're stored on the BD-side org row

and echoed in the register response and in GET /api/orgs/me. Use them

to maintain a permanent two-way mapping between your system's

Organisation/User ids and BD's orgId. Both fields also appear in the

org_created audit-log row so the cross-system reconciliation is part

of the hash chain.

Snake_case (external_org_id) and camelCase (externalOrgId) are both

accepted in the request body.

Idempotency — `Idempotency-Key` header

POST /api/orgs/register honors the standard Idempotency-Key header.

Send a fresh UUID per logical register attempt; if the same key arrives

again within the retention window, BD replays the original response

rather than creating a second pending org. Use to make retries safe

against flaky networks or worker restarts.

Constraints: 8–128 chars, alphanumerics + . _ - : only. Missing

header → endpoint runs without caching (idempotency disabled).

Self-service token rotation

POST /api/orgs/me/tokens/rotate (Bearer auth) revokes the org's

current token and mints a fresh one. Response: { token, rotatedAt }.

Also fires a token.rotated lifecycle webhook to the org's

webhook_url (if set), payload includes the new token so the

integrator can update its stored value automatically.

Dev/test convenience — `BD_TEST_PHONES` env

For testing loops where the operator has only one real phone and

wants to register many orgs against it. Set the env var to a

comma-separated list of E.164 numbers; F02 phone-uniqueness is

bypassed for any of them.

# /opt/butt-dial/packages/server/.env

BD_TEST_PHONES=+972526557547,+15551234567

Every bypass logs org_register_phone_collision_bypassed_via_whitelist

at WARN so prod misuse is loud. Numbers not on the whitelist

continue to enforce F02 as normal.

This bypass is independent of force: true — works for anonymous

register calls too. Don't set it on prod-prod; intended for staging /

dev environments only.

Dev/test convenience — `"force": true` on register

Super-admin callers can pass "force": true in the register body.

When set, BD auto-abandons any colliding pending org (email

match OR phone match) before proceeding. Lets you spin up many

fresh test orgs from one real email + phone without manually

calling /api/orgs/abandon between attempts.

Refuses to auto-abandon active orgs — those still throw

409 EMAIL_ALREADY_REGISTERED so a real customer's data is never

destroyed by a typo.

POST /api/orgs/register

Authorization: Bearer <super-admin>

Content-Type: application/json

{

"orgName": "Test #5",

"ownerEmail": "you+test5@example.com",

"ownerPhone": "+15551234567",

"force": true // ← auto-abandon any colliding pending org

}

Audit logs the abandon as org_register_force_abandoned_pending_email

or ..._pending_phone with the previous orgId.

Anonymous callers can't set this flag — force is silently ignored

unless superAdminMode is also true (verified via Bearer token).

Abandon a stuck signup — `POST /api/orgs/abandon`

When an integrator's user retries a signup after a previous attempt

left a pending org holding the unique resources (phone, email),

the integrator hits PHONE_ALREADY_REGISTERED or EMAIL_PENDING

on the retry. To unstick:

POST /api/orgs/abandon

Authorization: Bearer <super-admin OR pollToken>

Content-Type: application/json

{ "externalOrgId": "<integrator-side org id>" } // OR

{ "orgId": "<bd-side org uuid>" }

Two accepted credentials:

below are optional on this path. returned in the `/api/orgs/register` response (`{ pollToken: "..." }`).

Persist it alongside the BD org id on your side; it is single-org-scoped

and auto-cleared on /api/orgs/verify, so it can only authenticate

abandon for pre-verified orgs — exactly the scope this endpoint allows.

Required headers on the integrator path (matches /reveal-team-token

for one signing pattern):

  X-Request-Timestamp: <epoch seconds, ±60s of BD's clock>

X-Request-Nonce: <unique string, never re-used within 5 min>

Without them you get 401 AUTH_REPLAY_HEADERS_REQUIRED. Reusing a

nonce within 5 min returns 401 AUTH_NONCE_REPLAY.

Effects: status = 'deleted', revokes tokens, consumes pending

verification links. The phone + email become invisible to F02/F04

collision checks, so the next register attempt succeeds.

Refuses with 409 ORG_ACTIVE_USE_DEACTIVATION if the target is

status='active' — for active orgs use the proper deactivation

flow (which notifies members etc.).

Idempotent on already-deleted orgs ({ ok: true, noop: true }).

Audit-logged as org_abandoned with `{ fromStatus, externalOrgId,

orgName, reason }` in the details.

Recommended integrator retry pattern:

1. POST /api/orgs/register  →  409 EMAIL_PENDING or PHONE_ALREADY_REGISTERED

(response carries existingOrgId/collidingOrgId

when caller is super-admin)

  1. integrator checks: is the colliding orgId one I created previously
AND is my side's onboarding state != ACTIVE?
  1. yes → POST /api/orgs/abandon { externalOrgId: <my-side-org-id> }
  2. retry /api/orgs/register → succeeds

Security

with timestamp + nonce replay defense

Recover a missed account.verified webhook — `POST /api/orgs/:orgId/reveal-team-token`

When BD has verified an org but the integrator missed the

account.verified webhook (Cloudflare 521, signature mismatch,

delivery retry exhausted), the integrator can recover the org's

team_token using the per-org webhook secret. No super-admin needed.

POST /api/orgs/<bd-org-id>/reveal-team-token

Authorization: Bearer <webhook_secret> # OR

X-BD-Webhook-Secret: <webhook_secret>

X-Request-Timestamp: <epoch seconds, ±60s>

X-Request-Nonce: <unique string, never re-used within 5 min>

Response:

{ "token": "...", "rotated": false, "orgId": "...", "orgName": "..." }

The webhook_secret is returned exactly once in the /api/orgs/register

response. Persist it on the integrator's Organisation row; it stays valid

for the life of the org and is also what signs inbound webhooks.

Replay defense: the timestamp must be within ±60s of BD's clock and

the nonce is one-shot (cached server-side for 5 minutes). Replaying

the exact same headers returns AUTH_NONCE_REPLAY.

Audit-logged as org_token_revealed with actor=integrator:webhookSecret:.

← Home