Overview

Note

AI Context

  • Complexity: Low to Medium (unregister/delegate carry authorization and lifecycle side effects)

  • Cost: Free. No auth endpoint incurs platform billing.

  • Async: No. All auth operations are synchronous, except that signup/password-forgot/unregister trigger emails sent out-of-band.

All endpoints in this group are served directly at /auth/<name> — not under the /v1.0 prefix used by every other resource in the API. A generated stub exists for each path under /v1.0/auth/... that always returns 404 ROUTE_NOT_FOUND with a message pointing at the correct un-prefixed path; this is intentional routing scaffolding, not a bug.

Endpoint Summary

Method

Path

Auth

Purpose

POST

/auth/signup

None

Self-service customer registration.

POST

/auth/email-verify

None

Verify the signup email token; provisions the first access key.

GET

/auth/email-verify

None

Serves an HTML page that auto-submits the verification token (link target from the verification email).

POST

/auth/email-verify-resend

None

Send a fresh verification link to an address whose original link expired or was lost.

POST

/auth/login

Username/password

Exchange agent credentials for a 7-day JWT. See Authentication quickstart.

POST

/auth/boot

None (direct hash)

Exchange a direct hash for a 4-hour resource-scoped JWT.

POST

/auth/boot/refresh

Direct token

Reissue a direct token without changing which resource it is bound to.

POST

/auth/password-forgot

None

Request a password reset email.

GET

/auth/password-reset

None

Serves the HTML password reset form (link target from the reset email).

POST

/auth/password-reset

None (reset token)

Complete the password reset using the emailed token.

POST

/auth/unregister

Token or Accesskey

Freeze the caller’s own account and schedule (or immediately execute) deletion.

DELETE

/auth/unregister

Token or Accesskey

Cancel a scheduled deletion and restore the account to active.

POST

/auth/delegate

Token (ProjectSuperAdmin)

Issue a short-lived support/investigation token scoped to another customer. See Authentication quickstart.

Note

AI Implementation Hint

All unauthenticated /auth/* endpoints (login, signup, email-verify, email-verify-resend, boot, password-forgot, password-reset) share a single IP-based rate limiter: up to approximately 10 requests/second per client IP, burst 20. /auth/unregister and /auth/delegate require authentication first, then are subject to their own, separately-tracked IP-based limiter at the same approximate rate (10 requests/second, burst 20). Exceeding either limiter returns 429 with reason RATE_LIMIT_EXCEEDED (see Error Reason Codes).

/auth/email-verify-resend carries an additional per-account limit on top of that shared IP tier: a 60-second cooldown between sends and a cap of 5 sends per 24 hours, both tracked per customer account rather than per IP. The IP tier alone cannot stop a distributed mailbomb aimed at one address. Requests that exceed the per-account limits still return 200 with an empty body — no email is sent, and nothing in the response distinguishes that outcome.

Note

AI Implementation Hint — response body shape differs by endpoint

/auth/unregister and /auth/delegate sit behind the shared Authenticate() middleware, so authentication/authorization failures at that layer (missing token, expired token, frozen account) return the standard structured error envelope described in Error Reason Codes ({"error": {"status", "reason", "message", "request_id", ...}}). However, validation failures raised inside the auth handlers themselves — e.g. a malformed JSON body on any endpoint, a wrong password on /auth/unregister, an invalid confirmation_phrase, a bad accepted_tos on signup — return a bare HTTP status code (400) with an empty response body, not the JSON error envelope. Do not assume error.reason is present on every 4xx from this group; check for an empty body first.

Auth & Account Lifecycle

signup          email-verify        (set password)        login / boot
+----------+   +--------------+    +----------------+    +--------------+
| initial  |-->|   active     |--->|    active      |--->|  JWT / key   |
+----------+   +--------------+    +----------------+    +--------------+
     |          ^                         |
     | no       | POST /auth/email-       | POST /auth/unregister
     | verify   | verify-resend           |
     | within   | then email-verify       v
     v          |                   +----------------+
+----------+    |                   |    frozen      |
| expired  |----+                   | (30-day grace) |
+----------+                        +----------------+
                                      |             |
                        DELETE /auth/unregister      (grace expires,
                                      |               or immediate:true)
                                      v                       v
                                +----------------+     +----------------+
                                |    active      |     |    deleted     |
                                +----------------+     +----------------+

For the full customer status lifecycle (including what gets cascade-deleted), see Customer — Account Deletion Lifecycle.

Signup — POST /auth/signup

Creates an unverified customer account and triggers a verification email. See the Signup quickstart for a walkthrough.

Request body

Field

Type

Description

email

String, Required

Email address for the new account. Must be unique across all customers.

accepted_tos

Boolean, Required

Must be true. Missing or false is rejected with 400.

name

String, Optional

Display name for the customer account.

detail

String, Optional

Additional description.

phone_number

String, Optional

Contact phone number (E.164 format recommended).

address

String, Optional

Mailing address.

webhook_method

String, Optional

HTTP method for webhook delivery: POST, GET, PUT, or DELETE.

webhook_uri

String, Optional

URI where webhook events will be delivered.

Response — 200 OK (success)

{
    "customer": { "id": "...", "email": "...", "status": "initial", ... },
    "accesskey": { "id": "...", "token": "vb_...", "name": "...", ... }
}

Response — 200 OK (silent failure, e.g. duplicate email)

{}

Note

AI Implementation Hint

Signup always returns HTTP 200, even on failure, to prevent email-enumeration attacks — a duplicate or invalid email produces an empty {} body rather than an error. The only true 400 is a structurally invalid request (missing email, missing/false accepted_tos, or malformed JSON). The returned accesskey.token is usable immediately; no separate POST /auth/login call is required. Signup also auto-provisions an empty OutboundConfig (blocking all outbound PSTN calls until explicitly configured) and auto-rolls back the entire signup (deletes the customer) if that provisioning step permanently fails.

Errors

Status

Cause

400

email missing, accepted_tos missing/false, or malformed JSON body.

Email Verify — POST / GET /auth/email-verify

Validates the verification token from the signup email, marks the customer’s email as verified, and transitions the account from initial to active.

POST request body

Field

Type

Description

token

String, Required

64-character lowercase hex verification token from the signup email.

Response — 200 OK

{
    "customer": { "id": "...", "email": "...", "email_verified": true, "status": "active", ... }
}

Errors

Status

Cause

400

token is missing, invalid, expired, or already used; or the request body is malformed JSON.

GET /auth/email-verify

Serves a static HTML confirmation page (the link target embedded in the verification email). The page reads token from the query string and, when the visitor clicks “Verify Email”, submits it to POST /auth/email-verify via client-side JavaScript. This is a convenience UI for humans following the email link — API integrations should call POST /auth/email-verify directly with the token extracted from the email/webhook.

Parameter

Location

Description

token

Query, Required

64-character lowercase hex string. An invalid/missing token returns 400 before the page is rendered.

If the token has expired or was already used, the page reports that the link is no longer valid and offers a form to request a new one, which submits to POST /auth/email-verify-resend.

Email Verify Resend — POST /auth/email-verify-resend

Issues a new verification token for an account whose original link expired or was never received, and emails it to the registered address. This is the recovery path out of the expired status: an account that missed the 72-hour verification window is not permanently lost.

Request body

Field

Type

Description

email

String, Required

The email address the account was registered with.

Response — 200 OK (always)

{}

Note

AI Implementation Hint

This endpoint always returns 200 with an empty body — for a registered address, an unknown address, an already-verified account, a rate-limited request, and even a malformed JSON body. That uniformity is deliberate: any differentiated response would turn an unauthenticated endpoint into an email-existence oracle. Do not treat 200 as confirmation that an email was sent, and do not build retry logic that assumes otherwise. Note that the uniformity covers the response content, not its timing: the work is synchronous, so an unknown address returns after a single database lookup while a recoverable one also performs an agent lookup, two cache writes and an email send, leaving a measurable latency difference. This residual is not specific to this endpoint: /auth/signup and /auth/password-forgot are synchronous and always-200 in the same way, and carry the same timing residual.

A resend is only performed when the account is unverified, is not frozen or deleted, and still has a live agent for the address. Sends are limited to one per 60 seconds and 5 per 24 hours per account, on top of the shared per-IP /auth/* limiter.

The new token is valid for 24 hours, the same as a token issued at signup. Resending does not invalidate an earlier token — any previously issued token stays usable until its own expiry, so a user who later finds the original email can still follow it.

Login (Token) — POST /auth/login

Exchanges an agent’s username/password for a JWT valid for 7 days. Fully documented with request/response examples in the Authentication quickstart; summarized here for completeness.

Field

Type

Description

username

String, Required

The agent’s username.

password

String, Required

The agent’s password.

Response: {"username": "...", "token": "eyJ..."}. The token is also set as an HttpOnly, Secure, SameSite=Strict cookie named token on the response.

Errors

Status

Cause

400

Missing fields, malformed JSON, or invalid credentials. Credential failures return a bare 400 with no response body — a wrong password is deliberately not distinguished from an unknown username, to avoid a username-enumeration oracle. A failure to look up the account’s status (see below) also returns this same bare 400.

403

The credentials were correct, but the account’s status forbids issuing a token: ACCOUNT_EXPIRED (email address was never verified, so the account was expired by the cleanup job) or ACCOUNT_DELETED. Unlike the 400 above, these carry a full error envelope, and ACCOUNT_EXPIRED carries details[0].recovery_endpoint — byte-identical to what the /v1.0/* gate returns for the same account.

Note

AI Implementation Hint — login can now fail on account status

POST /auth/login verifies the password first and only then checks the customer’s account status, so the status check never becomes an enumeration oracle. The status check is a deny-list: only expired and deleted are refused. initial (a normal user inside the 72-hour post-signup verification window) and frozen both still log in successfully — frozen in particular must keep working, because the frozen self-recovery endpoint DELETE /auth/unregister requires authentication, so blocking a frozen login would remove the only route to recovery.

The account-status lookup fails closed: if the customer lookup itself fails, no token is issued (returned as the same opaque 400, since it is not a determination about the account). This is deliberately the opposite of the /v1.0/* gate, which fails open — an outage must not become a window in which credentials can be minted freely.

Boot (Direct Token) — POST /auth/boot

Resolves a direct hash into a short-lived, resource-scoped JWT, for use cases where the end user has no VoIPBin account (e.g. an embedded AI voice widget on a customer’s public website). Also documented in the Authentication quickstart.

Request body

Field

Type

Description

direct_hash

String, Required

The direct hash link, e.g. direct.a1b2c3d4e5f6. Obtained from a direct link URL or a resource’s direct_hash field (e.g. GET /v1.0/webchat_widgets/{id}).

Response — 200 OK

Field

Type

Description

token

String

JWT for API/WebSocket use. Pass as Bearer <token>.

type

String

Always "direct".

resource_type

String

The resource type this token is scoped to (e.g. "ai", "ai_team", "webchat_widget").

resource_id

UUID

The scoped resource’s ID.

customer_id

UUID

The owning customer’s ID.

expire

String (ISO 8601)

Token expiry. Valid for 4 hours from issuance.

allowed_resource_id

UUID

The single resource this token may act on, assigned at boot before the resource exists. The resource you create with this token takes this ID. You do not need to send it anywhere: the server takes the target from the token, so two visitors of the same public link cannot reach each other’s conversation.

scope_version

Integer

Version of the token’s scope contract. If it is raised, every outstanding token becomes invalid at once and clients must boot again.

resource_data

Object, Optional

Present only for resource types with a registered public-display fetcher (currently webchat_widget). Contains a public_display_config key with anonymous-visitor-safe display settings (e.g. theme). Omitted entirely when there is nothing to report; a fetch failure never fails the boot request itself.

What a direct token can and cannot do

A direct token is bound to exactly one resource: the allowed_resource_id above.

  • Creating a resource (POST /aicalls, POST /webchat_sessions) produces a resource with that ID. A second creation attempt with the same token returns 409; boot again for a new assignment.

  • Requests that name a resource in the path (e.g. DELETE /aicalls/{id}) are refused with 403 unless the ID matches the assignment.

  • Requests that name a resource in the body or query (e.g. POST /aimessages) ignore the value you send and use the assignment instead.

  • Listing (GET /aicalls, GET /webchat_sessions) is not available to direct tokens.

  • WebSocket topics of the form customer_id:<customer_id>:<resource_type>:<resource_id> are accepted only when <resource_id> is exactly the assignment, written as a canonical lower-case UUID. A trailing colon or a partial ID is refused.

Errors

Status

Cause

400

direct_hash missing/empty, does not start with direct., does not resolve to any resource, resolves to an unsupported resource type, or the owning customer is not active.

Note

AI Implementation Hint

Boot tokens are scoped: they only grant access to the resource types listed in the token (e.g. aicall for an ai/ai_team direct hash, webchat_session for a webchat_widget direct hash). For WebSocket subscriptions, boot tokens may only subscribe to 4-part topics (customer_id:<uuid>:<resource_type>:<resource_id>); broader topics are rejected.

Boot refresh (Direct Token) — POST /auth/boot/refresh

Reissues a direct token while keeping the same allowed_resource_id, so a conversation in progress survives the 4-hour token expiry. Call it before the token expires; the widget SDKs do this automatically.

Unlike POST /auth/boot this endpoint is authenticated: present the current direct token as Bearer <token>. The assignment is copied from that token and is never read from the request body, so a caller cannot name a resource it does not already hold.

Request body

None.

Response — 200 OK

Same shape as POST /auth/boot, except resource_data is omitted (the client already holds it from the original boot). allowed_resource_id and scope_version are unchanged; only expire moves.

Refreshing does not extend a session indefinitely

Each boot fixes an absolute ceiling and every reissued token inherits it unchanged, so refreshing cannot walk it forward. A refresh is also refused, with 401, when any of the following became true since the original boot:

  • the boot session’s absolute lifetime has elapsed,

  • the direct link was deleted, or its hash was regenerated (this is how you revoke a leaked public link),

  • the owning customer is no longer active,

  • the token predates resource binding and carries no assignment.

Errors

Status

Meaning

401

The token can no longer be refreshed. Boot again.

403

The presented token is not a direct token.

500

The server could not issue a token. Retrying the refresh may succeed; booting again will not help.

Password Forgot — POST /auth/password-forgot

Generates a password reset token and emails a reset link to the agent, valid for 1 hour.

Request body

Field

Type

Description

username

String, Required

The agent’s username (email address).

Response — 200 OK: always {}, regardless of whether the username exists, to prevent username-enumeration attacks. The underlying call to bin-agent-manager is made best-effort; a lookup failure is logged but never surfaced to the caller.

Errors

Status

Cause

400

username missing or malformed JSON body.

Password Reset — GET / POST /auth/password-reset

Completes a password reset using the token emailed by POST /auth/password-forgot.

GET /auth/password-reset — serves the HTML reset form (the link target in the reset email).

Parameter

Location

Description

token

Query, Required

64-character lowercase hex string. An invalid/missing token returns 400 before the form is rendered.

POST /auth/password-reset — request body

Field

Type

Description

token

String, Required

64-character lowercase hex reset token from the email. Single-use, expires 1 hour after issuance.

password

String, Required

New password. Minimum 8 characters.

Response — 200 OK: {}.

Errors

Status

Cause

400

token missing/invalid/expired, password missing or under 8 characters, malformed JSON body, or (per upstream agent-manager policy) the token belongs to a guest agent — guest agents cannot reset their password.

Unregister (Self-Service Deletion) — POST / DELETE /auth/unregister

Self-service account freeze/deletion and recovery. Requires authentication (Token or Accesskey) and PermissionCustomerAdmin on the caller’s own customer account; direct (boot) tokens cannot call this endpoint. A condensed narrative version of this section, including the full status-lifecycle diagram and the cascade-delete resource list, lives in Customer — Account Deletion Lifecycle; this section is the field-level contract.

POST /auth/unregister — request body

Field

Type

Description

password

String, Conditional

Re-authentication password for password-based accounts. Mutually exclusive with confirmation_phrase — exactly one of the two must be supplied.

confirmation_phrase

String, Conditional

Must be exactly "DELETE". Used for SSO/accesskey-authenticated requests that have no password to re-check.

immediate

Boolean, Optional

Default false. If true, skips the 30-day grace period: the account is frozen and then permanently deleted (PII anonymized, all resources cascade-deleted) in the same request. Irreversible.

Query parameters (both POST and DELETE)

Parameter

Location

Description

accesskey

Query, Optional

Access-key token, as an alternative to a Bearer token / token= credential.

Response — 200 OK: the updated Customer object (see Customer), reflecting status: "frozen" (or "deleted" if immediate was true).

Errors

Status

Cause

400

Neither or both of password/confirmation_phrase supplied; password re-authentication failed; confirmation_phrase is not exactly "DELETE"; malformed JSON body; caller lacks PermissionCustomerAdmin; caller is authenticated via a direct (boot) token (DIRECT_ACCESS_NOT_SUPPORTED); or the downstream freeze/delete call failed. The handler returns a bare 400 for all of these cases — it does not distinguish permission/direct-access failures with a 403, unlike POST /auth/delegate.

401

Missing/invalid/expired token or access key (returned by the shared Authenticate() middleware, as a structured error envelope — see the response-shape note above).

403

The account’s customer status is expired or deleted. Password re-authentication runs the same login path that refuses those two statuses (VOIP-1491), so the refusal surfaces here instead of failing a step later. Both were already dead ends on this endpoint — the freeze transition requires active — so this only makes the refusal earlier and honest.

500

The customer lookup performed during password re-authentication failed for any other reason (RPC timeout, or the customer-manager circuit breaker being open). That lookup fails closed, unlike the /v1.0/* gate’s, so an unreachable customer-manager blocks the request rather than letting it through.

Note

403 and 500 are reachable only through the password branch, which re-authenticates by calling AuthLogin; before VOIP-1491 that branch could only ever produce a 400. The confirmation_phrase branch does not re-authenticate and is unaffected.

DELETE /auth/unregister — cancels a scheduled deletion and restores active status. No request body. Only works while the account is frozen within the 30-day grace period.

Response — 200 OK: the restored Customer object.

Errors

Status

Cause

400

Account is not currently in frozen state; caller lacks PermissionCustomerAdmin; or caller is authenticated via a direct (boot) token. As with POST /auth/unregister above, the handler returns a bare 400 for all of these cases rather than a 403.

401

Missing/invalid/expired token or access key.

Note

AI Implementation Hint — frozen and deleted accounts are also enforced globally

Once a customer account is frozen, every other authenticated /v1.0/* request from that customer (except from a PermissionProjectSuperAdmin, and except direct/boot tokens, which skip the check entirely) is rejected with 403 ACCOUNT_FROZEN by the shared authentication middleware — not just calls to resource endpoints that would otherwise mutate data. The error’s details[0] carries deletion_scheduled_at, deletion_effective_at (30 days after scheduling), and recovery_endpoint: "DELETE /auth/unregister" so client UIs (admin/talk consoles) can render a consistent “account frozen, recover here” screen. POST and DELETE /auth/unregister themselves are explicitly exempted from this block so a frozen customer can still self-recover.

The same middleware also rejects any authenticated request from a deleted customer with 403 ACCOUNT_DELETED. In the steady state this is unreachable — a deleted customer’s agents/access keys should already have been soft-deleted by the customer_deleted cascade and fail earlier at authentication — but this is a deliberate second layer in case that cascade misses a resource, so credentials belonging to a deleted account can never keep working indefinitely. Unlike frozen, deleted is not recoverable via /auth/unregister.

An expired customer — signup completed but the email address was never verified, so the unverified-account cleanup job moved the account out of initial — is rejected the same way, with 403 ACCOUNT_EXPIRED. Its details[0] carries recovery_endpoint: "POST /auth/email-verify-resend", so a client can point the user at a new verification email without string-matching the message. initial is not blocked: it is the normal state during the 72-hour verification window. Note that requesting a resend requires the account to still have a live agent for the address; if it does not, direct the user to support@voipbin.net rather than telling them to sign up again, because an account expired under the current cleanup behaviour keeps its customer row live and will fail the signup duplicate-email check.

The gate’s customer lookup fails open: if the customer record cannot be fetched, the request is allowed through rather than blocked, because api-manager holds no customer cache and failing closed would take every /v1.0/* route down whenever customer-manager is unreachable. Occurrences are counted by the api_manager_account_status_lookup_failed_total metric, labelled by identity_type and error_class. POST /auth/login deliberately does the opposite and fails closed — see the login section above.

Delegate (Superadmin Support Access) — POST /auth/delegate

Lets a platform superadmin (PermissionProjectSuperAdmin) issue a short-lived, audit-logged token that grants PermissionCustomerAdmin-equivalent access scoped to a specific target customer, for support/investigation without needing that customer’s own credentials. Fully documented with request/response examples in the Authentication quickstart; summarized here for completeness.

Request body

Field

Type

Description

customer_id

String (UUID), Required

Target customer to act on behalf of.

reason

String, Required

Justification, written to the audit log. Must be 10-200 printable-ASCII characters (0x20-0x7E), no control characters.

Response — 200 OK

{
    "token": "eyJ...",
    "customer_id": "...",
    "expire": "2026-05-19T06:00:00.000000Z"
}

The delegate token is valid for 8 hours and grants customer-admin-equivalent access scoped only to customer_id — no project-level permissions are carried over.

Errors

Status

Cause

400

Malformed or missing request body.

401

Caller is not authenticated.

403

Caller lacks PermissionProjectSuperAdmin; or the caller is itself authenticated via a delegate token (recursive delegation is blocked).

404

Target customer does not exist or is already deleted.

422

customer_id is not a valid UUID, or reason fails length/character validation.

Note

AI Implementation Hint

Every delegate token issuance (and denial) is written to the audit log with audit=true, the issuer’s agent ID, target customer, reason, and expiry — this is the audit trail for superadmin account access. Do not build tooling that calls this endpoint without a genuine, specific support reason; the reason field is not free-form filler, it is the compliance record.