Skip to content

API reference

This page is generated from the code (backend/tools/generate_api_docs.py), do not edit it by hand. Descriptions come from the backend docstrings. Concepts (authentication, multi-domain, roles, errors) are described in the API documentation; the interactive OpenAPI spec is served on /docs by the API.

513 operations, 42 groups.

Contents

Authentication

Login, invitation-based registration, profile, OAuth (Google, Microsoft, Apple).

POST /auth/forgot-password

Request a password reset.

Rate limited to 5 per hour per e-mail address (per client address without an e-mail), whatever the X-API-Key or Authorization headers.

The body must be a JSON object (else 400 validation_error); an email that is missing or not a string gets 400 validation_error too.

The link goes through the SMTP server of the account's own domain and leads to that domain's first production hostname, whatever X-Frontend-Host: the single domain that holds every organization of the account (for a domain_admin, also every domain it administers, and only when the request comes from that domain). A super_admin, or an account spread over several domains or with none, gets the platform's server and FRONTEND_URL (SMTP_* environment variables); without a configured server nothing is sent. The answer is the same in every case.

Access: public (no authentication)
Rate limit: 5 per hour

Request body

Name Description
email User email address.

Response: Success message (always returns success to prevent email enumeration).

POST /auth/login

Login with email and password.

Rate limited by the FAILED sign-ins only (a good sign-in is never counted, so nobody can lock an account out with the owner's own successes, and the owner's sign-ins do not eat the budget): 30 failures per 15 minutes per e-mail address (RATELIMIT_LOGIN_ACCOUNT), then 429 rate_limited with Retry-After, even for the right password (the budget bounds the guesses). Where the client's address is known (not behind the TLS demultiplexer, see "Limits" in the API concepts) two more budgets apply: 10 failures per 15 minutes per e-mail address and client address, and 100 per 15 minutes per client address, so one address cannot lock an account out for the others nor spray many accounts. The answers to the 4th and later failures of an account are slowed (0.5 s, doubling, up to 4 s). A request refused with 400 validation_error is not counted. The bucket is the e-mail address, whatever the X-API-Key and Authorization headers.

The body must be a JSON object (else 400 validation_error); an email or a password that is missing or not a string gets 400 validation_error too.

Session length: the access token lasts 1 hour. The refresh token lasts 30 days (JWT_REFRESH_TOKEN_EXPIRES), renewed at each POST /auth/refresh. With remember false it lasts only 12 hours (JWT_REFRESH_TOKEN_EXPIRES_SHORT, JWT_REFRESH_SHORT_HOURS), still renewed at each refresh: the session ends after 12 hours without a refresh. The choice is kept by the rotation of the refresh token and by the new tokens of PUT /auth/me/password and PUT /auth/me/email.

Access: public (no authentication)
Rate limit: 30 per 15 minutes; 10 per 15 minutes; 100 per 15 minutes

Request body

Name Description
email User email address.
password User password.
remember Optional boolean, default true (30-day refresh token, the behavior before this option existed). false opens a short session (12 hours). Another type gets 400 validation_error with field remember.
any_domain Optional, default false. Sign-in from a client that does not know the domain of the account (the mobile app when nobody typed another domain): the answer then carries domain, the domain the account works in, whose host the client adopts as X-Frontend-Host. The domain of X-Frontend-Host when the account works there, otherwise the only domain the account works in (with several, the one of its current organization). A failed sign-in is answered exactly as without it (same 401, same budgets, same delay): it tells nothing about the existence of an account. Ignored when the request carries an X-API-Key header. Another type gets 400 validation_error with field any_domain. Second factor: when the account has one (authenticator app, passkey), the right password does not open the session; the answer is mfa_required with an mfa_token (5 minutes, single use) to send with a code or a passkey assertion to POST /auth/mfa/verify. When the account must have one (platform and domain admins, and organization admins where the domain asks) and its grace period is over, the answer is mfa_enrollment_required with an mfa_token (15 minutes) for the enrollment routes (header X-MFA-Token). Today the platform does not enforce it (MFA_ENFORCE off): such an account without a second factor gets its session with mfa_setup (deadline null), never mfa_enrollment_required; once enforced, mfa_setup carries the deadline of the grace period (14 days).

Response: user: The user (as GET /auth/me). access_token: Access JWT (1 hour). Sent as Authorization: Bearer. refresh_token: Refresh JWT (30 days, 12 hours when remember is false), renewed at each POST /auth/refresh. domain: Only with any_domain: {id, name, host} of the domain to use (host: first public hostname, as domain_host of GET /api/v1/organizations/my), null when the account works in no domain and the host is unknown. mfa_setup: Only for an account that should have a second factor and has none (recommended while not enforced, or during the grace period): {required, reason (platform_admin, domain_admin or organization_admin), deadline (null while not enforced)}. mfa_required (boolean): Instead of the session: the account has a second factor; with mfa_token, methods (totp, webauthn, backup_code) and expires_in (seconds). mfa_enrollment_required (boolean): Instead of the session: the account must add a second factor first; with mfa_token, reason, deadline and expires_in. mfa_token: The token of the second step (never a session token).

POST /auth/logout

Log out: revoke the refresh token given in the Authorization header and the whole session it belongs to: the access tokens still alive are refused at once (401), those of the earlier rotations included. Other sessions of the user are not touched. (A session opened before this change keeps its access token until it expires, within the hour.)

Rate limited like POST /auth/refresh: per user of the refresh token.

Access: refresh JWT

Response: success: True.

GET /auth/me

Get current authenticated user.

The user object (also returned by login, registration, OAuth and the user routes) has these fields.

With the header X-Admin-Organization (super admin or domain admin, see "Administration context" in the API concepts) the user is shown working in that organization: organization_id is the administered one, team_id is null, the role is the platform role, and the response also carries admin_context and admin_organization_id. Nothing is written on the account.

In a session an administrator opened as this user (POST /api/v1/organizations/<org_id>/members/<user_id>/impersonate), the user also carries impersonated_by {id, name, email}: the administrator.

Access: access JWT

Response: user: mfa (second factor of the account: enabled, methods, totp, passkeys, backup_codes_left, required, reason, deadline, enforced), id, email, name, first_name, last_name, phone (null, or an object with country, number in E.164 and verified_at), timezone (IANA name, null until chosen), language (fr or en), role (agent, manager, admin, domain_admin or super_admin: a copy derived from the profiles and platform permissions, never what decides), permissions (former permission names given by the platform permissions or the profiles in the current organization, plus those granted to the account such as support), organization_id, team_id, oauth_provider, email_verified (false only for a free sign-up whose address was not proved yet), is_active, is_service, created_at, last_login, default_organization_id, default_organization_ids, domain_ids, platform_permissions (platform_admin, domain_admin, support: the permissions of the account), organization_permissions (the permissions of the user in their current organization, given by their profiles; every one for an administrator of the platform or of its domain), profile_ids (the profiles of their membership there), dnd (the do not disturb of the account: {active, until, set_at}, as GET /auth/me/dnd). Accounts created before first_name and last_name existed get them deduced from name. admin_context: true, only while the header X-Admin-Organization is honored. admin_organization_id: The administered organization, with admin_context.

PATCH /auth/me

Update current user profile.

The body must be a JSON object (else 400 validation_error). A field set to null is left unchanged, except timezone, phone and default_organization_id, which null clears.

Changing name alone rewrites first_name and last_name (first word, then the rest); changing first_name or last_name alone rewrites name; sending name with either keeps first_name and last_name as sent.

The profile cannot change the e-mail address (PUT /auth/me/email), grant permissions (an account never grants itself any) or verify a phone number (PUT /auth/me/phone). Deprecated: changing the password here (use PUT /auth/me/password); it now requires current_password and, like the new route, ends the other sessions and follows the same strength rule (400 validation_error with field password). The requests that carry a password are limited to 10 per hour per user (429 rate_limited), like PUT /auth/me/password: they are guesses at the current password for whoever holds a stolen token. The other updates keep the default limits.

Access: access JWT
Rate limit: 10 per hour

Request body

Name Description
name New display name (optional, a non-empty string).
first_name First name (optional, at most 100 characters).
last_name Last name (optional, at most 100 characters; may be empty).
language Language preference (optional, fr or en).
timezone IANA time zone such as America/Toronto (optional; null clears it).
phone Phone number as an object with number and optional country (ISO code; for a North American number it is deduced from the area code, CA or US, when absent; other countries must be allowed by the domain and need the international format), or null to remove it. Normalized to E.164; verified_at is null until PUT /auth/me/phone proves it, and an unchanged number keeps its verified_at. Invalid number gets 400 invalid_phone.
permissions Refused with 403 forbidden unless identical to the current list.
password Deprecated, see PUT /auth/me/password (optional; the strength rule of that route).
current_password Current password, required with password (401 invalid_password if wrong).
default_organization_id Default organization for the current domain (optional, a string; null clears it). The organization must belong to the domain of the request (X-Frontend-Host) and the user must be a member of it, unless the account has the platform permission platform_admin. A field of another type, or a shorter password, gets 400 validation_error with field.

Response: Updated user data. default_organization_id is the default for the current domain, default_organization_ids maps every domain to its default. Only when the password changed, also access_token and refresh_token of a new session (the current one stays signed in, the others end).

PUT /auth/me/email

Change the e-mail address (the sign-in identifier) of the current user.

Needs the proof that the user controls the new address: POST /auth/verifications with channel email, then POST /auth/verifications/<id>/confirm, whose verified_token is sent here (single use, valid 10 minutes). The current password is required too. Rate limited to 10 per hour per user. The body must be a JSON object.

The address is the key of the sign-in: every session ends (token_version is incremented) except the current one, which continues on the new tokens returned here. Pending password reset links are cancelled and the former address gets a notice (best effort).

Access: access JWT
Rate limit: 10 per hour

Request body

Name Description
email The new address.
password The current password (not needed by an account without password).
verified_token Token of a confirmed verification of this address (purpose email_change).

Response: user: The user with the new address. access_token: New access token. refresh_token: New refresh token, to store in place of the old one.

Errors 400 validation_error (field email or password), 400 verification_required (no verified_token), 400 invalid_code (token unknown, already used or for another address), 400 code_expired (token older than 10 minutes), 401 invalid_password, 409 email_in_use, 429 rate_limited.

PUT /auth/me/password

Change the password of the current user.

Replaces PATCH /auth/me with a password, which is now deprecated. Rate limited to 10 per hour per user. The body must be a JSON object (else 400 validation_error).

The new password must have 8 to 128 characters (72 bytes in UTF-8 at most), mix at least 2 of lower case, upper case, digits and other characters, not be a very common password, not be the e-mail address or its local part and differ from the current password.

Every other session ends (token_version is incremented, so their access and refresh tokens are refused with 401). The current session continues on the new tokens returned here, which keep its remember choice: the client must store them in place of the old ones. An account without a password (OAuth sign-in only) does not send current_password and gets its first password this way.

Access: access JWT
Rate limit: 10 per hour

Request body

Name Description
current_password The password in use (old_password is accepted as a synonym).
new_password The new password, see the rule above.

Response: success: True. user: The user. access_token: New access token. refresh_token: New refresh token, to store in place of the old one.

Errors 400 validation_error with field new_password when the rule is not met, and with field current_password when it is missing. 401 invalid_password when current_password is wrong. 429 rate_limited.

PUT /auth/me/phone

Set the verified phone number of the current user.

Needs the proof that the user receives texts on this number: POST /auth/verifications with channel sms, then POST /auth/verifications/<id>/confirm, whose verified_token is sent here (single use, valid 10 minutes). The current password is required too. Rate limited to 10 per hour per user. The body must be a JSON object. The sessions are not affected.

Access: access JWT
Rate limit: 10 per hour

Request body

Name Description
country ISO code of the country (deduced from the area code, CA or US, for a North American number; other countries must be allowed by the domain and need the international format).
number The number, E.164 or national digits; stored in E.164.
verified_token Token of a confirmed verification of this number (purpose phone_change).
password The current password (not needed by an account without password).

Response: user: The user, with phone set to the number and verified_at to now.

Errors 400 invalid_phone (bad number, country not allowed), 400 validation_error (field password), 400 verification_required (no verified_token), 400 invalid_code (token unknown, already used or for another number), 400 code_expired (token older than 10 minutes), 401 invalid_password, 429 rate_limited.

GET /auth/oauth/apple

Get the Sign in with Apple authorization URL.

Public, never rate limited. Apple must be enabled for the domain of the request with every credential (client ID, team ID, key ID, private key: the domain's, else the platform's), else 400 oauth_error. Apple sends the browser back with a POST (form_post, required to receive the name and the address) to the API's POST /auth/oauth/apple/return, which sends it on to redirect_uri with code and state (and user, the first time). The state is signed by the server (10 minutes, single use, bound to Apple, this domain and redirect_uri) and its nonce must come back in Apple's id_token. The client keeps state and refuses a return whose state differs.

Access: public (no authentication)
Rate limit: exempt from the general per-address limit

Query parameters

Name Description
redirect_uri The web page the browser ends on (an address of a known domain), which sends the code to POST /auth/oauth/apple/callback.

Response: auth_url: The Apple consent URL to send the browser to. state: The state in that URL (to be checked at the callback; the callback requires it).

POST /auth/oauth/apple/callback

Handle the Sign in with Apple callback.

Rate limited by the default limits, counted per authorization code (per client address without a code), whatever the X-API-Key or Authorization headers. The body must be a JSON object (else 400 validation_error).

Apple must be enabled with every credential for the domain of the request, else 400 oauth_error and nothing is sent to Apple. The token exchanges with Apple are also limited to 30 per minute for the whole platform: beyond, 429 rate_limited. Each request to Apple waits at most 10 seconds.

The state of GET /auth/oauth/apple must come back with the code (else 400 oauth_error, before anything is sent to Apple); it works once. Apple's id_token is checked (signature with Apple's public keys, issuer, client ID, expiry, nonce of the state). The account is the one linked to the Apple sub; an existing account with the same address is linked (Apple vouches for the address, a private relay address included) unless it is linked to another Apple identity; an account linked to Google or Microsoft gets the Apple identity next to it. A new account is created only where sign-up is open on the domain. Then the second step, as POST /auth/login (mfa_required, mfa_enrollment_required). Every failure after the state is the same 400 oauth_error with a generic message.

Access: public (no authentication)

Request body

Name Description
code Authorization code from Apple (query of the web page).
state The state returned by GET /auth/oauth/apple.
user The user value Apple sent the first time (its name), optional.
any_domain As POST /auth/login.

Response: user: The user (as GET /auth/me). access_token: Access JWT (1 hour). refresh_token: Refresh JWT (30 days). domain: Only with any_domain, as POST /auth/login. mfa_setup, mfa_required, mfa_enrollment_required, mfa_token: As POST /auth/login (second factor).

POST /auth/oauth/apple/return

Where Apple posts the browser back (form_post): sends it on to the web page of the state.

Public, never rate limited. The default limits counted it per client address, that is one counter for every Apple sign-in of the platform behind the TLS demultiplexer (a form body has no target the anonymous routes are keyed by). A per-state key would protect nothing: anyone can post made-up states, each one a new counter, and each costs a signature check only. The route sends nothing to Apple and spends nothing: the signed state (10 minutes, bound to Apple and to the redirect_uri given to GET /auth/oauth/apple, a page of a known domain) only lets it send the browser on to that page; the code is exchanged, once, by POST /auth/oauth/apple/callback, which has its limits. The form (code, state, user the first time, error) comes from Apple through the browser. Nothing is exchanged here: the state is checked (signature, Apple, not expired) without being spent, then 303 to the redirect_uri given to GET /auth/oauth/apple with code, state and user in the query (or error). A state that does not check gets 400 oauth_error.

Access: public (no authentication)
Rate limit: exempt from the general per-address limit

Request body

Name Description
code Authorization code (form field).
state The state of GET /auth/oauth/apple (form field).
user JSON with the name, the first time only (form field).
error Apple's error, for example user_cancelled_authorize (form field).

GET /auth/oauth/google

Get Google OAuth authorization URL.

Public, never rate limited. The URL carries a state made and signed by the server (valid 10 minutes, single use, bound to this provider, this domain and redirect_uri) and a PKCE challenge (S256) whose verifier only the server knows. The client keeps state (for example in sessionStorage) before redirecting the browser, refuses a return whose state differs, and sends it to POST /auth/oauth/google/callback.

Access: public (no authentication)
Rate limit: exempt from the general per-address limit

Query parameters

Name Description
redirect_uri Callback URL after authorization.

Response: auth_url: The Google consent URL to send the browser to. state: The state in that URL (to be checked at the callback; the callback requires it).

POST /auth/oauth/google/callback

Handle Google OAuth callback.

Rate limited by the default limits, counted per authorization code (per client address without a code), whatever the X-API-Key or Authorization headers. The body must be a JSON object and its fields strings (else 400 validation_error).

Google must be enabled, with a client ID, in the OAuth settings of the domain of the request, and the platform's Google credentials configured: else 400 oauth_error, and nothing is sent to Google. The token exchanges with Google are also limited to 30 per minute for the whole platform (only the exchanges actually sent count): beyond, 429 rate_limited. Each request to Google waits at most 10 seconds for an answer.

The state of GET /auth/oauth/google must come back with the code (else 400 oauth_error, before anything is sent to Google); it works once. The account is the one linked to the Google sub; an existing account with the same address is linked only when Google says the address is verified (email_verified) and the account is not linked to another Google identity. A new account is created only where sign-up is open on the domain (settings.signup.enabled), in that domain, as the admin of its own organization. Every failure after the state is the same 400 oauth_error with a generic message.

Access: public (no authentication)

Request body

Name Description
code Authorization code from Google.
state The state returned by GET /auth/oauth/google.
redirect_uri Callback URL used in authorization (must be the one of the state).
any_domain As POST /auth/login (the answer then carries domain).

Response: user: The user (as GET /auth/me). access_token: Access JWT (1 hour). refresh_token: Refresh JWT (30 days). domain: Only with any_domain, as POST /auth/login. mfa_setup, mfa_required, mfa_enrollment_required, mfa_token: As POST /auth/login (second factor).

GET /auth/oauth/microsoft

Get Microsoft OAuth authorization URL.

Public, never rate limited. The URL carries a state made and signed by the server (valid 10 minutes, single use, bound to this provider, this domain and redirect_uri) and a PKCE challenge (S256) whose verifier only the server knows. The client keeps state (for example in sessionStorage) before redirecting the browser, refuses a return whose state differs, and sends it to POST /auth/oauth/microsoft/callback.

Access: public (no authentication)
Rate limit: exempt from the general per-address limit

Query parameters

Name Description
redirect_uri Callback URL after authorization.

Response: auth_url: The Microsoft consent URL to send the browser to. state: The state in that URL (to be checked at the callback; the callback requires it).

POST /auth/oauth/microsoft/callback

Handle Microsoft OAuth callback.

Rate limited by the default limits, counted per authorization code (per client address without a code), whatever the X-API-Key or Authorization headers. The body must be a JSON object and its fields strings (else 400 validation_error).

Microsoft must be enabled, with a client ID, in the OAuth settings of the domain of the request, and the platform's Microsoft credentials configured: else 400 oauth_error, and nothing is sent to Microsoft. The token exchanges with Microsoft are also limited to 30 per minute for the whole platform (only the exchanges actually sent count): beyond, 429 rate_limited. Each request to Microsoft waits at most 10 seconds for an answer.

The state of GET /auth/oauth/microsoft must come back with the code (else 400 oauth_error, before anything is sent to Microsoft); it works once. The account is the one linked to the Microsoft object id. Microsoft vouches for no mail address, so an existing account is never linked by address (it signs in with its password; an account created by Microsoft sign-in is marked oauth_email_verified false), and a pending domain admin invitation is not applied. A new account is created only where sign-up is open on the domain (settings.signup.enabled), in that domain. Every failure after the state is the same 400 oauth_error with a generic message.

Access: public (no authentication)

Request body

Name Description
code Authorization code from Microsoft.
state The state returned by GET /auth/oauth/microsoft.
redirect_uri Callback URL used in authorization (must be the one of the state).
any_domain As POST /auth/login (the answer then carries domain).

Response: user: The user (as GET /auth/me). access_token: Access JWT (1 hour). refresh_token: Refresh JWT (30 days). domain: Only with any_domain, as POST /auth/login. mfa_setup, mfa_required, mfa_enrollment_required, mfa_token: As POST /auth/login (second factor).

GET /auth/oauth/providers

Get list of enabled OAuth providers for current domain.

Public, never rate limited (a cheap read of the domain settings, loaded by every login page).

A provider is offered only when its callback would accept a sign-in (the same rule, AuthService.oauth_provider_enabled): Google and Microsoft when the domain turns them on and the platform's client ID and secret for them are configured (sign-in always uses them); Apple when the domain enables it and its four credentials (client ID, team ID, key ID, private key: the domain's, each missing one taken from the platform's) are known. A button is never shown that would end in 400 oauth_error.

Access: public (no authentication)
Rate limit: exempt from the general per-address limit

Response: providers: {google, microsoft, apple}, true for each provider offered on the domain of the request.

POST /auth/refresh

Refresh the session.

Requires the refresh token in the Authorization header. The refresh token is rotated: the returned one replaces it (the old one stays valid 60 seconds, for tabs that refresh at once). The tokens of one sign-in form a family (claim fam, kept by the rotation): a refresh token already rotated and presented again after those 60 seconds is a copy, and the whole family is revoked (401 for it, the newest tokens and the access tokens included: the owner signs in again). A disabled account gets 401. Once the second factor is enforced on the platform (MFA_ENFORCE), a session that never proved one, of an account that must have one and whose grace period is over, is no longer renewed (401: sign in again, with the second factor); never today. The new tokens keep the claims of the session (remember, family, second factor proved, impersonation). Rate limited by the default limits, counted per user (the identity of the refresh token; per client address when the token is invalid).

Access: refresh JWT

Response: access_token: New access token. refresh_token: New refresh token, to store in place of the old one.

POST /auth/register

Register a new user with an invitation token.

Rate limited to 10 per hour per invitation token (per client address without a token), whatever the X-API-Key or Authorization headers.

The body must be a JSON object and its fields strings: anything else gets 400 validation_error.

Access: public (no authentication)
Rate limit: 10 per hour

Request body

Name Description
token Invitation token (required).
password User password. It follows the rule of PUT /auth/me/password (8 to 128 characters, at least 2 of lower case, upper case, digits and symbols, not a very common password, not the e-mail address): else 400 validation_error with field password.
name User display name.
language Preferred language (optional).

Response: user: The new user (as GET /auth/me). access_token: Access JWT (1 hour). Sent as Authorization: Bearer. refresh_token: Refresh JWT, to renew the session with POST /auth/refresh.

Errors 402 plan_limit: the invitation gives the messages (messages.read or messages.send) and the organization's messaging plan has no room left for one more user (its place was kept while it was pending: this happens when the plan changed since); {limit (users), plan, upgrade, current}. No account is created and the invitation stays usable.

POST /auth/reset-password

Reset password using a reset token.

Rate limited to 10 per hour per reset token (per client address without a token), whatever the X-API-Key or Authorization headers.

The body must be a JSON object (else 400 validation_error); a token or a password that is missing or not a string gets 400 validation_error too. The new password follows the same rule as PUT /auth/me/password (8 to 128 characters, 2 character classes out of 4, not a very common password, not the e-mail address: 400 validation_error with field password).

A link works once and is taken atomically before the password changes: two simultaneous requests with the same link cannot both succeed (the second gets 400 validation_error "Invalid or expired reset token"). A successful reset ends every session of the account (token_version) and cancels the account's other pending links. An account created by a sign-in whose address the provider did not vouch for (oauth_email_verified false) loses its OAuth link: the reset proves the mailbox is the person's.

Access: public (no authentication)
Rate limit: 10 per hour

Request body

Name Description
token Password reset token.
password New password. It follows the rule of PUT /auth/me/password (8 to 128 characters, at least 2 of lower case, upper case, digits and symbols, not a very common password, not the e-mail address): else 400 validation_error with field password, and the token stays usable.

Response: Success message.

POST /auth/verifications

Send a verification code to an e-mail address or a phone number.

The first step of PUT /auth/me/email and PUT /auth/me/phone. A code has 6 digits, is valid 10 minutes and is stored hashed. An e-mail goes through the SMTP server of the user's domain, a text message through the SMS provider of that domain, from a number of its pool that no customer holds (or the one named by the domain setting verification.sms_from); it is not charged to the organization.

Asking again for the same target resends a fresh code on the same verification (same id, attempts reset, previous code void). Limits per hour: 3 codes per user and target, 10 per user, 6 per mailbox whoever asked (the spellings of one mailbox count as one: the +tag of an address and the dots of a Gmail address); at least 60 seconds between two codes (resend_after); 30 requests per hour per user. Beyond, 429 rate_limited with retry_after seconds (and a Retry-After header). The body must be a JSON object.

Access: access JWT
Rate limit: 30 per hour

Request body

Name Description
channel email or sms (required).
target The e-mail address, or the phone number (E.164 or national digits).
purpose email_change (the default for email), phone_change (the default for sms) or email_verify (proof of the address the account has, for a free sign-up whose address is not proved yet: no target, or the account's own address; 409 already_verified once proved; confirming it grants the starting credit that waited); the purpose must match the channel.
country ISO code of the country of an sms target (deduced from the area code, CA or US, for a North American number).

Response: verification: Object with id, channel, purpose, target_masked (the target with its middle hidden), expires_in (seconds the code is valid) and resend_after (seconds before another code can be asked). Status 202.

Errors 400 validation_error (field channel, target or purpose), 400 invalid_phone, 429 rate_limited, 502 send_failed (the provider or the mail server refused), 503 email_unavailable (no SMTP server for the domain), 503 sms_unavailable (the domain has no SMS number or provider to send the code with).

POST /auth/verifications/<verification_id>/confirm

Confirm a verification code and get the verified token.

A code allows 5 attempts and 10 minutes; after that, ask for a new code with POST /auth/verifications. Rate limited to 60 per hour per user. The body must be a JSON object. A verification of another user is a 404.

Access: access JWT
Rate limit: 60 per hour

Path parameters

Name Description
verification_id Verification ID (the id returned by POST /auth/verifications).

Request body

Name Description
code The 6 digits received.

Response: verification: Object with id and verified (true). verified_token: Single-use token proving the target, valid 10 minutes, to send to PUT /auth/me/email or PUT /auth/me/phone (already spent for the purpose email_verify). expires_in: Seconds the verified_token is valid. email_verified: true, only for the purpose email_verify (the account is marked verified). credit_granted: The starting credit granted by this confirmation, only when there was one.

Errors 400 invalid_code with attempts_left (wrong code), 400 code_expired (code older than 10 minutes), 404 not_found, 409 already_verified (confirmed already, ask for a new code), 429 too_many_attempts (5 attempts used) or rate_limited.

Second factor and passkeys

Second step of the sign-in, authenticator app (TOTP), backup codes, passkeys (WebAuthn: fingerprint, face, security key) and passwordless sign-in. Closed to API keys.

GET /auth/mfa

The second factor of the account: what is set up, and whether it must have one.

Session, or X-MFA-Token (enrollment).

Access: public (no authentication)
API keys: refused, 403 forbidden (API key not allowed on this route)

Response: mfa: enabled, methods, totp, passkeys (count), backup_codes_left, required, reason (platform_admin, domain_admin, organization_admin), deadline, enforced. passkeys: The passkeys of the account: id, name, created_at, last_used_at, rp_id (the site it works on), backed_up, transports.

POST /auth/mfa/backup-codes

Replace the backup codes (the old ones stop working).

Session only, for an account with a second factor (else 409 mfa_not_enabled). Proof: a TOTP code or a backup code; the password for an account with passkeys only and no code left.

Access: public (no authentication)
API keys: refused, 403 forbidden (API key not allowed on this route)
Rate limit: 20 per 15 minutes

Request body

Name Description
code A TOTP code or a backup code.
password The password (only for an account with passkeys alone).

Response: backup_codes: The 10 new codes, shown once.

POST /auth/mfa/totp/confirm

Confirm the authenticator app with a code it shows: it becomes a second factor.

Session, or X-MFA-Token (enrollment: the answer then opens the session). 400 invalid_code, 409 not_started. Rate limited: 20 failures per 15 minutes per account.

Access: public (no authentication)
API keys: refused, 403 forbidden (API key not allowed on this route)
Rate limit: 20 per 15 minutes

Request body

Name Description
code The 6-digit code (required).

Response: mfa: The status (as GET /auth/mfa). backup_codes: The 10 backup codes, shown once, when the account had none. access_token, refresh_token: The session, marked as having proved a second factor (new tokens of the current session; with X-MFA-Token, also user and domain as POST /auth/login).

POST /auth/mfa/totp/disable

Remove the authenticator app.

Session only. Proof: a code of the app or a backup code. 409 mfa_required when it is the last factor of an account that must have one.

Access: public (no authentication)
API keys: refused, 403 forbidden (API key not allowed on this route)
Rate limit: 20 per 15 minutes

Request body

Name Description
code A TOTP code or a backup code (required).

Response: mfa: The status after the change.

POST /auth/mfa/totp/setup

Start setting up an authenticator app (TOTP, 6 digits, 30 seconds).

Session, or X-MFA-Token (enrollment). A new secret each time, kept pending until confirmed; 409 already_enabled when the account has one. An account that already has a second factor adds another one only from a session that proved one (else 403 second_factor_required).

Access: public (no authentication)
API keys: refused, 403 forbidden (API key not allowed on this route)

Response: secret: The secret in base32 (to type by hand). otpauth_uri: The otpauth:// URI. qr_svg: The QR code of that URI (data:image/svg+xml URI).

POST /auth/mfa/verify

Second step of a sign-in: prove the second factor and open the session.

Public. The mfa_token of POST /auth/login (or of an OAuth callback) answered mfa_required: 5 minutes, single use, refused (401 invalid_mfa_token) once the password of the account changed. Rate limited: 10 failures per 15 minutes per account (whatever the mfa_token), then 429.

Access: public (no authentication)
API keys: refused, 403 forbidden (API key not allowed on this route)
Rate limit: 10 per 15 minutes

Request body

Name Description
mfa_token The token of mfa_required (required).
method totp, backup_code or webauthn (required).
code totp and backup_code: the 6-digit code of the authenticator app, or a backup code (single use).
challenge_id webauthn: the challenge_id of POST /auth/mfa/webauthn/options.
credential webauthn: the PublicKeyCredential of navigator.credentials.get (JSON).

Response: user, access_token, refresh_token, domain: As POST /auth/login. The session carries the second factor. backup_codes_left: After a backup code, how many are left.

POST /auth/mfa/webauthn/options

Options of navigator.credentials.get for the second step of a sign-in (the passkeys of the account).

Public. Rate limited: 60 per 15 minutes per account of the mfa_token (the requests with a token that does not read share one counter of their own), never per address. The Origin of the request must be a known domain (the mobile app sends no Origin but X-Client-App: ios or android with X-Frontend-Host); else 400 origin_not_allowed.

Access: public (no authentication)
API keys: refused, 403 forbidden (API key not allowed on this route)
Rate limit: 60 per 15 minutes

Request body

Name Description
mfa_token The token of mfa_required (required).

Response: challenge_id: To send back with the credential (single use, 5 minutes). options: The PublicKeyCredentialRequestOptions (JSON, binary fields in base64url).

PATCH /auth/webauthn/credentials/<credential_id>

Rename a passkey of the account.

Session only.

Access: public (no authentication)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
credential_id Id of the passkey (GET /auth/mfa).

Request body

Name Description
name The new name (60 characters).

Response: passkey: The passkey.

POST /auth/webauthn/credentials/<credential_id>/remove

Remove a passkey of the account.

Session only. Proof: a TOTP code or a backup code; the password for an account with passkeys only and no code left. 409 mfa_required when it is the last factor of an account that must have one.

Access: public (no authentication)
API keys: refused, 403 forbidden (API key not allowed on this route)
Rate limit: 20 per 15 minutes

Path parameters

Name Description
credential_id Id of the passkey.

Request body

Name Description
code A TOTP code or a backup code.
password The password (only for an account with passkeys alone).

Response: mfa: The status after the change.

POST /auth/webauthn/login

Sign in with a passkey (fingerprint, face, PIN of the device, security key).

Public. The authenticator must have verified the person (user verification): the sign-in counts as a multi-factor one, the session opens at once (claim mfa). Unknown passkey, passkey of another site or refused signature: 401. Rate limited, failures (401) only: 30 per 15 minutes per passkey presented (credential id), and, when the client's address is known, 30 per 15 minutes per address; never one counter for the whole platform. Then 429 rate_limited.

Access: public (no authentication)
API keys: refused, 403 forbidden (API key not allowed on this route)
Rate limit: 30 per 15 minutes; 30 per 15 minutes

Request body

Name Description
challenge_id The challenge_id of POST /auth/webauthn/login/options (required).
credential The PublicKeyCredential of navigator.credentials.get (JSON, required).
remember As POST /auth/login.
any_domain As POST /auth/login.

Response: user, access_token, refresh_token, domain: As POST /auth/login.

POST /auth/webauthn/login/options

Options of navigator.credentials.get for a sign-in with a passkey (no address, no password).

Public. Rate limited: 60 per 15 minutes per client address when that address is known; not limited when it is not (a per-address counter would then be one for the whole platform: one person would stop everybody's passkey sign-in). The challenge lives 5 minutes. The Origin of the request must be a known domain (the mobile app sends no Origin but X-Client-App: ios or android with X-Frontend-Host: the passkeys of that site); else 400 origin_not_allowed. The browser offers the passkeys it has for this site.

Access: public (no authentication)
API keys: refused, 403 forbidden (API key not allowed on this route)
Rate limit: 60 per 15 minutes

Response: challenge_id: To send back with the credential (single use, 5 minutes). options: The PublicKeyCredentialRequestOptions (JSON, user verification required).

POST /auth/webauthn/register

Add a passkey (the answer of navigator.credentials.create).

Session, or X-MFA-Token (enrollment: the answer then opens the session). 400 invalid_challenge or invalid_credential, 409 already_registered.

Access: public (no authentication)
API keys: refused, 403 forbidden (API key not allowed on this route)
Rate limit: 20 per 15 minutes

Request body

Name Description
challenge_id The challenge_id of POST /auth/webauthn/register/options (required).
credential The PublicKeyCredential (JSON, required).
name A name to recognize it (60 characters, default "Clé d'accès").

Response: passkey: The passkey (as in GET /auth/mfa). backup_codes: The 10 backup codes, shown once, when the account had none. access_token, refresh_token: As POST /auth/mfa/totp/confirm. mfa: The status after the change.

POST /auth/webauthn/register/options

Options of navigator.credentials.create to add a passkey to the account.

Session, or X-MFA-Token (enrollment). The Origin of the request must be a known domain: the passkey works on that site (the mobile app sends X-Client-App: ios or android with X-Frontend-Host instead; else 400 origin_not_allowed). 409 too_many_passkeys beyond 20. An account that already has a second factor adds another one only from a session that proved one (else 403 second_factor_required).

Access: public (no authentication)
API keys: refused, 403 forbidden (API key not allowed on this route)

Response: challenge_id: To send back with the credential (single use, 5 minutes). options: The PublicKeyCredentialCreationOptions (JSON, binary fields in base64url).

Account (preferences, deletion, sign-up)

Notification preferences, free preferences, deleting one's own account and free sign-up.

DELETE /auth/me

Delete one's own account.

What happens, organization by organization: when the account is the only member, the organization closes with it (refused with 409 balance_negative while its balance is below zero); its API keys, integrations, widgets, invitations, SIP endpoints and call routing are removed and its phone numbers go back to the pool of the domain. When the account shares an organization, it leaves it (refused with 403 last_admin when it is the last member holding members.admin, such as the Admin profile, in an organization that still has other members: give members.admin to someone else first). Every session of the account ends. A confirmation is e-mailed.

The account stays recoverable for a grace period (settings.account_deletion.grace_days of the domain, 10 days by default, 0 for none): signing in again with the password during it cancels the deletion (the numbers still free in the pool come back); afterwards the account is deleted for good.

Refused in the administration context (X-Admin-Organization, 403 forbidden: it is a personal action), and for super admins and domain admins (403 forbidden).

Access: access JWT
Rate limit: 5 per hour

Request body

Name Description
password The password of the account (not needed for an account that signs in with Google or Microsoft only). 401 invalid_password when wrong.
confirm Must be true (400 validation_error, field confirm).

Response: success: true. deletion: {state: scheduled (recoverable until purge_at) or deleted, purge_at: UTC date or null, numbers: the phone numbers released}.

GET /auth/me/dnd

The do not disturb of the account (the mobile app's switch).

While it is on: no push for new text messages nor for missed calls, the voicemail push arrives silent, and every endpoint of the account (in all its organizations) is in do not disturb (its calls go to its busy handling). The e-mails are not affected. It ends by itself at until.

Access: access JWT

Response: dnd: {active (boolean), until (ISO 8601 in UTC with Z, or null: no end), set_at (ISO 8601 in UTC with Z: when it was turned on, null when off)}. An expired one is {active: false, until: null, set_at: null}.

PUT /auth/me/dnd

Turn the do not disturb of the account on or off.

Every endpoint of the account, in all its organizations, follows: in do not disturb until until (or without end), or out of it when turned off. The other sessions of the account get the Socket.IO event dnd_update {dnd}.

Access: access JWT

Request body

Name Description
active true to turn it on, false to turn it off (required). Anything else: 400 validation_error with field active.
until With active true: the end, an ISO 8601 date and time with its time zone (Z or +hh:mm), in the future and at most 7 days from now (otherwise 400 validation_error with field until); null or absent: no end. Ignored with active false.

Response: dnd: The state after the change, as GET /auth/me/dnd.

GET /auth/me/notifications

The notification preferences of the account: which e-mails and pushes it receives.

The platform defaults apply to what the person never changed (a domain can set its own defaults, settings.notifications.defaults). Messages that carry a security notice, a requested code, an invitation or a service notice (password reset, verification codes, the negative balance notices, the end of a trial...) are always sent and have no switch.

Access: access JWT

Response: notifications: email ({updates, survey, purchase, login, low_balance}: booleans; low_balance_threshold: the balance under which the low-balance e-mail is sent, null to use the one of the organization) and push ({sms, missed_call, voicemail}: booleans).

PATCH /auth/me/notifications

Change the notification preferences (partial: only the keys sent change).

Access: access JWT

Request body

Name Description
email Optional object with any of updates, survey, purchase, login, low_balance (booleans) and low_balance_threshold (a number from 0 to 100000, or null to use the organization's threshold).
push Optional object with any of sms, missed_call, voicemail (booleans). An unknown channel or key, a value of the wrong type, or a body that is not an object gets 400 validation_error with field (for example email.login).

Response: notifications: All the preferences after the change, as GET /auth/me/notifications.

GET /auth/me/preferences

The free preferences of the account: a small key/value store for the apps (display choices, last used filters...). Nothing in it changes what the server does.

Access: access JWT

Response: preferences: The object, empty until something is stored.

PATCH /auth/me/preferences

Change the free preferences: every key of the body replaces the stored one, a null value removes it, the other keys are left alone.

Access: access JWT

Request body

Name Description
<key> Any JSON value (keys of 1 to 64 letters, digits, _ or -; values nested four levels at most, with keys of the same kind). At most 50 keys and 8 KB in all. A key or a value that is not acceptable, or too much data, gets 400 validation_error with field.

Response: preferences: The whole object after the change.

POST /auth/signup

Create an account, its organization and its default team (free sign-up).

Closed by default: a domain admin opens it (settings.signup.enabled of the domain, 403 signup_disabled otherwise). Protections: a captcha when the domain set a provider, at most 5 accounts per client address and per day (setting max_per_ip_per_day), disposable e-mail providers refused when the domain lists them, 10 attempts per hour and e-mail address (and 20 per hour and client address). The "client address" is the real one only when the infrastructure passes it on (see "Limits" in the API concepts): behind the TLS demultiplexer of the production host it is unknown, the per-address cap and bucket do not apply, and the sign-ups share a ceiling per domain and day of 100 or 10 times max_per_ip_per_day. The phone, if given, is not verified (confirm it with /auth/verifications); a person with no phone number can sign in.

The e-mail address is not proved at sign-up (the user gets a session at once; user.email_verified is false). The starting credit (setting initial_credit) is therefore NOT granted now: it is granted, once, when the address is verified with POST /auth/verifications (purpose email_verify, no target) then POST /auth/verifications/<id>/confirm, or by a verified change of address. A 409 email_in_use tells that an address has an account: the sign-up form needs it, the captcha and the limits above bound the probing.

Access: public (no authentication)
Rate limit: 10 per hour; 20 per hour

Request body

Name Description
email E-mail address (required, it is the sign-in).
password Password (required; 8 to 128 characters, 2 classes of characters, not a common password nor the e-mail).
first_name First name (required).
last_name Last name (required).
phone Optional object {number, country?}, normalized to E.164 and unverified (invalid: 400 invalid_phone).
language fr or en (default fr).
accept_terms Must be true (400 validation_error, field accept_terms).
captcha_token The answer of the captcha widget (required when the domain set a provider; 400 captcha_failed when wrong).

Response: 201 with user, access_token and refresh_token, like a login. Welcome e-mail (setting notifications.welcome_email) as the domain decided; the starting credit (setting initial_credit of the domain) waits for the verified e-mail address (see above).

Errors 404 domain_not_found (no domain matches X-Frontend-Host), 403 signup_disabled, 403 email_domain_blocked, 400 validation_error / invalid_phone / captcha_failed, 409 email_in_use, 429 rate_limited, 503 captcha_unavailable (the provider could not be asked).

GET /auth/signup/config

What the sign-up page needs to know about the domain: whether sign-up is open and the captcha to show. Public.

Access: public (no authentication)
Rate limit: exempt from the general per-address limit

Response: signup: {enabled, captcha: {provider (none, recaptcha or turnstile), site_key}, terms_url}. Sign-up is closed (enabled false) unless a domain admin opened it.

Errors 404 domain_not_found: no domain matches X-Frontend-Host.

Organizations

The user's organizations and administration of the domain's organizations.

GET /api/v1/organizations

Search and list the organizations of the current domain (platform_admin or domain_admin).

The current domain is the one serving the frontend host (e.g. other.telma.ca), so each whitelabel site only shows its own organizations. Sorted by name (case and accents ignored). A platform_admin can search every domain with all_domains.

Access: platform permission platform_admin or domain_admin
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Query parameters

Name Description
q Free text, searched anywhere in the organization name or in the e-mail address of its owner, ignoring case and accents (optional, 100 characters at most, else 400 validation_error with field q).
skip Pagination offset (default 0). Not an integer: 400 validation_error with field skip.
limit Page size (default 50, max 100; a larger value is brought back to 100). Not an integer: 400 validation_error with field limit.
all_domains true to search every domain instead of the current one (platform_admin only, 403 forbidden for a domain_admin; true or false, else 400 validation_error).

Response: organizations: The organizations of the page, each with member_count, team_count, owner_email (null without owner) and, with all_domains, domain_name (display name of its domain) and domain_host (hostname to use for that domain); domain_id is always there. total: Number of organizations matching the filters, before skip and limit. skip: The offset applied. limit: The page size applied.

POST /api/v1/organizations

Create a new organization in the current domain (platform_admin, or domain_admin of that domain).

Access: platform permission platform_admin or domain_admin
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
name Organization name (required, a string of 1 to 120 characters once trimmed: 400 validation_error otherwise).
owner_email Email of the owner (optional). For a domain admin, only an account that already belongs to this domain (a membership in one of its organizations) is taken; any other address is ignored, without telling whether it exists.

Response: organization: The created organization. message: Confirmation.

DELETE /api/v1/organizations/<org_id>

Delete organization (platform_admin, or domain_admin of its domain).

Refused (400) while the organization has members. Every other access goes with it: its integrations and their API keys, website widgets, pending invitations and voice endpoints; its phone numbers go back to the domain. Its teams and conversations stay stored, out of reach.

Access: platform permission platform_admin or domain_admin
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
org_id Organization ID.

Response: Success message.

GET /api/v1/organizations/<org_id>

Get organization details.

platform_admin: any organization; domain_admin: those of their domains; a member holding members.admin or organization.settings: their current organization only.

Not available to restricted API keys (a key limited to a team, or an integration's key): 403 with error forbidden.

Access: authenticated (JWT or API key); platform_admin, domain_admin of the domain of the organization, or members.admin or organization.settings in that organization when it is the current one (checked in the function: 403 forbidden otherwise).
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
org_id Organization ID.

Response: organization: The organization with its teams; members (with their e-mail addresses) only for a platform_admin, a domain_admin of its domain or a member holding members.admin there (the same rule as GET /organizations/current/members): without it, the answer has no members.

PUT /api/v1/organizations/<org_id>

Update organization (platform_admin, or domain_admin of its domain).

Access: platform permission platform_admin or domain_admin
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
org_id Organization ID.

Request body

Name Description
name New organization name (a string of 1 to 120 characters once trimmed: 400 validation_error otherwise; absent or null leaves it unchanged).

Response: organization: The updated organization. message: Confirmation.

PUT /api/v1/organizations/<org_id>/billing-anniversary

Update the billing anniversary date for an organization.

Access: platform permission platform_admin
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
org_id Organization ID.

Request body

Name Description
billing_anniversary ISO date string or null.

Response: Updated organization.

PUT /api/v1/organizations/<org_id>/capabilities

Update capabilities for an organization.

Only a platform_admin, or a domain_admin of the organization's domain, can update capabilities. Organization capabilities must be a subset of the domain's capabilities.

The known capabilities are textos (messaging), voxos (voice service), sip (SIP endpoints and softphone provisioning, /voice/endpoints* and /voice/provision), call_codes (call codes, legacy without interface: no app uses them any more) and voicemail_boxes (voicemail boxes of the endpoints and independent boxes, /voice/mailboxes). sip, call_codes and voicemail_boxes need voxos in the same list. The list replaces the previous one. Removing sip (or voxos) disables the SIP endpoints of the organization; they are enabled again when sip returns. Every route that returns an organization shows its list.

Not available to restricted API keys (a key limited to a team, or an integration's key): 403 with error forbidden.

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
org_id Organization ID.

Request body

Name Description
capabilities List of capabilities, each one of textos, voxos, sip, call_codes, voicemail_boxes (e.g., ["textos", "voxos", "sip"]). An unknown name, a missing list, or sip, call_codes or voicemail_boxes without voxos is refused with 400 validation_error (field capabilities); a capability the domain does not have, too.

Response: organization: The updated organization. message: Confirmation.

GET /api/v1/organizations/<org_id>/invitations

Get invitations for an organization.

platform_admin: any organization; domain_admin: those of their domains; a member holding members.admin or organization.settings: their current organization only.

Not available to restricted API keys (a key limited to a team, or an integration's key): 403 with error forbidden.

Access: authenticated (JWT or API key); platform_admin, domain_admin of the domain of the organization, or members.admin or organization.settings in that organization when it is the current one (checked in the function: 403 forbidden otherwise).
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
org_id Organization ID.

Response: invitations: The invitations of the organization. total: Number of invitations.

DELETE /api/v1/organizations/<org_id>/members/<user_id>

Delete a member from an organization (platform_admin, or domain_admin of its domain).

The user must be a member of that organization (a membership there, whatever their current organization; else 403). The member must be deactivated first: 409 with error user_active otherwise. 403 member_of_other_organizations when the account also belongs to organizations the caller does not administer.

Access: platform permission platform_admin or domain_admin
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
org_id Organization ID.
user_id User ID.

Response: Success message.

PUT /api/v1/organizations/<org_id>/members/<user_id>

Update a member in an organization (platform_admin, or domain_admin of its domain).

The user must be a member of that organization (a membership there, whatever their current organization; else 403): the role changes on the membership of THIS organization (users.role follows only when it is their current one). Never one's own role or status, never a platform account (super admin, domain admin) unless the caller is a super admin (403 forbidden).

Access: platform permission platform_admin or domain_admin
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden; refused when the body gives the role admin, domain_admin, super_admin

Path parameters

Name Description
org_id Organization ID.
user_id User ID.

Request body

Name Description
role New role for the user (optional; admin, manager or agent, another value is ignored): the member's profiles are replaced by those of the role (admin: Admin; manager: User and the organization's Manager profile; agent: User). Never on a platform_admin or domain_admin account (403). An API key never gives the admin role (403).
is_active Whether the user is active (optional).

Response: user: The updated member. message: Confirmation.

Errors 402 plan_limit: the role gives the messages (messages.read or messages.send) to a member who did not have them and the organization's messaging plan has no room left (as POST /api/v1/invitations): {limit (users), plan, upgrade, current}; nothing is changed. 403 member_of_other_organizations: activating or deactivating the account reaches organizations the caller does not administer.

POST /api/v1/organizations/<org_id>/members/<user_id>/impersonate

Sign in as a member of the organization, without their password ("Se connecter en tant que").

For a member holding members.admin in the organization (the Admin profile, or a profile of its own that gives it), a domain admin of its domain or a super admin, with a session (never an API key, never from a session itself opened as a member). The member must be active, and every organization they belong to must be one the caller administers (a domain admin only by a super admin, a super admin never). The answer has the shape of POST /auth/login: a new session of the member, short (no "remember me"), whose user carries impersonated_by (the caller). It does not count as the member's sign-in, it ends when the caller's account is deactivated, and it may not change the password, e-mail or phone of the account (nor ask for a verification code), delete it, create an API key or sign in as someone else in turn (403 impersonation_forbidden). Journaled in audit_logs (type impersonation_started).

Access: authenticated (JWT or API key)
API keys: refused, 403 forbidden (API key not allowed on this route)
Rate limit: 20 per hour

Path parameters

Name Description
org_id Organization ID.
user_id The member's user ID.

Response: user: The member (as GET /auth/me), with impersonated_by {id, name, email}. access_token: Access token of the new session. refresh_token: Refresh token of the new session (12 hours without a refresh).

Errors 403 forbidden: not an administrator of the organization (members.admin), oneself, an API key or a session opened as a member. 403 member_of_other_organizations: the member also belongs to organizations the caller does not administer. 404 not_found: unknown organization, or not a member. 409 user_inactive: the account is deactivated.

GET /api/v1/organizations/current

Get current user's organization for the current domain.

If user's current organization doesn't belong to the current domain, automatically switch to the user's default organization (if it belongs to this domain), or fall back to any organization in the current domain (if available).

Header X-Organization-Fallback: none turns off the last resort above and, for a super admin or a domain admin, every implicit organization: they are no longer put in the first organization of the domain, nor kept in the one an earlier session left on their account (which is forgotten: the account has no current organization afterwards). The answer is 404 not_found and the client shows its organization picker; the administrator works in an organization by administering it (X-Admin-Organization, below). Other users are not affected.

While the header X-Admin-Organization is honored (super admin or domain admin administering an organization), the organization is the administered one: no domain check and no automatic switch, nothing is written on the account, and the response carries admin_context: true and admin_organization_id.

Not available to restricted API keys (a key limited to a team, or an integration's key): 403 with error forbidden.

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: organization: Organization data with member_count and team_count. admin_context: true, only while the header X-Admin-Organization is honored. admin_organization_id: The administered organization, with admin_context.

PUT /api/v1/organizations/current

Rename the current organization (permission organization.settings).

Not available to restricted API keys (a key limited to a team, or an integration's key): 403 with error forbidden.

Access: authenticated (JWT or API key); Permission organization.settings in the current organization (profiles; checked in the function: 403 forbidden otherwise).
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
name New organization name (required, a string of 1 to 120 characters once trimmed: 400 validation_error otherwise).

Response: organization: The updated organization. message: Confirmation.

GET /api/v1/organizations/current/credit-status

Get credit status for current organization.

Not available to restricted API keys (a key limited to a team, or an integration's key): 403 with error forbidden.

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: credit_balance: Credit balance of the organization. has_credits: Whether credits are available. credit_hold_count: Number of messages on credit hold.

GET /api/v1/organizations/current/members

Get members of current user's organization.

Not available to restricted API keys (a key limited to a team, or an integration's key): 403 with error forbidden.

Access: authenticated (JWT or API key); Permission members.admin in the current organization (profiles; checked in the function: 403 forbidden otherwise).
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: members: The members of the organization. total: Number of members.

GET /api/v1/organizations/my

List all organizations the current user belongs to in the current domain.

Not available to restricted API keys (a key limited to a team, or an integration's key): 403 with error forbidden.

An account with the platform permission platform_admin or domain_admin also gets, without being a member, every organization it administers (platform_admin: those of the current domain, or of every domain with all_domains; domain_admin: those of their domains), with its platform role (super_admin, domain_admin) as role. Not with member_only. Use GET /organizations (q, skip, limit) to search among them without the 1000-organization cap of this list. While the header X-Admin-Organization is honored, is_current marks the administered organization and the response carries admin_context and admin_organization_id.

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Query parameters

Name Description
all_domains If "true", return organizations across all domains (not just current).
member_only If "true", only the organizations where the user sees at least one team in the member view of the mobile app (by the profiles held there: with teams.all every team, otherwise only the team they are placed in, nothing without one; a platform permission adds nothing); the mobile app lists those.

Response: organizations: List of organizations with user's role in each (organization role, or the platform role for an organization that is only administered), permissions (the permissions of the user there, given by their profiles; every one for an organization they administer from the platform or its domain), their domain (domain_id, domain_name, domain_host: hostname to use for that domain), created_at and is_current. total: Number of organizations listed.

POST /api/v1/organizations/switch

Switch to a different organization.

Not available to restricted API keys (a key limited to a team, or an integration's key): 403 with error forbidden.

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
organization_id ID of the organization to switch to.

Response: user: The user, now working in the organization. organization: The organization. message: Confirmation.

Errors 400 validation_error: organization_id missing. 404 not_found: unknown organization. 403 forbidden: not a member (and not an administrator of it). 500 server_error: unexpected failure.

GET /api/v1/organizations/unread-counts

Get unread message counts per organization in the current domain.

Only the conversations of the team the user is a member of in each organization count (the same rule as the push notifications): being able to read the other teams (teams.all, platform permissions) does not make their messages a notification.

Not available to restricted API keys (a key limited to a team, or an integration's key): 403 with error forbidden.

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: unread_counts: Number of unread messages per organization id.

Teams

Teams, members, business hours and team webhook.

GET /api/v1/teams

List teams accessible to current user in the current domain.

A member holding teams.all in the organization (profiles Admin and Manager) gets its teams; otherwise only the team they are placed in (the mobile app needs its name and organization). A restricted API key (limited to a team, or an integration's key) gets the teams it can reach only, never with their webhook secret, whatever the profiles of its owner and whatever organization its owner currently works in. webhook_secret is given team by team, to the members holding teams.provider in the team's organization (profiles Admin and Manager) and to the platform_admin and domain_admin accounts who administer it.

Access: authenticated (JWT or API key)

Query parameters

Name Description
organization_id Organization whose teams to list (optional; default: the caller's current organization). Except for a platform_admin, it must be an organization the caller belongs to or, for a domain admin, an organization of their domains: any other one gets 403, whatever X-Frontend-Host. With a restricted API key, it only filters the teams of the key (never 403).
skip Number of teams to skip (optional, default 0; a negative value counts as 0, a value over 2147483647 is refused: 400 validation_error).
limit Number of teams (optional, default: every team; max 100, a larger value is brought back to 100, 0 or less to 1). A skip or limit that is not an integer is refused (400 validation_error with field).

Response: teams: The teams of the page. total: Number of teams before skip and limit. skip, limit: The values applied (limit null when absent).

POST /api/v1/teams

Create a new team in the caller's current organization.

400 validation_error for a caller without organization. An organization with a messaging plan creates no more than its max_teams: 402 plan_limit with limit (teams), plan and upgrade (the next plan, or null).

Access: permission manage_teams (now teams.manage)

Request body

Name Description
name Team name (required, a string of 1 to 120 characters once trimmed: 400 validation_error otherwise).
phone_number Team phone number.

Response: team: The created team, with its webhook_secret. message: Confirmation.

DELETE /api/v1/teams/<team_id>

Delete a team.

Access: team access (team_id); permission manage_teams (now teams.manage)

Path parameters

Name Description
team_id Team ID.

Response: Success message.

GET /api/v1/teams/<team_id>

Get team details.

Access: permission members.manage in the organization (profiles); team access (team_id)

Path parameters

Name Description
team_id Team ID.

Response: team: The team; its webhook_secret and provider configuration only for a caller holding teams.provider in the team's organization (or administering it from the platform or its domain).

PUT /api/v1/teams/<team_id>

Update team settings.

Access: team access (team_id); permission configure_hours (now teams.hours)

Path parameters

Name Description
team_id Team ID.

Request body

Name Description
name Team name (optional; a string of 1 to 120 characters once trimmed: 400 validation_error otherwise).
phone_number Team phone number (optional).
business_hours Business hours config (optional).
outside_hours_behavior Behavior setting (optional).
auto_reply_message Auto-reply message (optional).
welcome_message Message sent when a conversation starts (optional).
timezone IANA time zone of the business hours and of the archives (optional, e.g. America/Montreal: 400 validation_error when unknown).
message_templates Canned messages agents can insert, list of {title, body} (optional, replaces the list).

Response: Updated team data.

PUT /api/v1/teams/<team_id>/hours

Update team business hours.

Access: team access (team_id); permission configure_hours (now teams.hours)

Path parameters

Name Description
team_id Team ID.

Request body

Name Description
business_hours List of {day, start, end} objects.
outside_hours_behavior queue (a message received outside business hours is shown marked "queued", with the usual notifications) or reject (shown marked "rejected", without push notification); auto_reply, an older value, behaves like queue.
auto_reply_message Text sent back automatically to a message received outside business hours, whatever the behavior (once a day per conversation; optional).
timezone IANA time zone for business hours (e.g., America/Montreal; 400 validation_error when unknown).

Response: Updated team data.

GET /api/v1/teams/<team_id>/members

List members of a team (super admins, by their platform permissions, are never listed, except to themselves).

Access: permission members.manage in the organization (profiles); team access (team_id)

Path parameters

Name Description
team_id Team ID.

Response: members: The members of the team. total: Number of members.

POST /api/v1/teams/<team_id>/members

Add a member to a team.

Access: team access (team_id); permission manage_teams (now teams.manage)

Path parameters

Name Description
team_id Team ID.

Request body

Name Description
user_id User ID to add to the team.

Response: members: The members of the team after the change. message: Confirmation.

DELETE /api/v1/teams/<team_id>/members/<user_id>

Remove a member from a team.

Access: team access (team_id); permission manage_teams (now teams.manage)

Path parameters

Name Description
team_id Team ID.
user_id User ID to remove from the team.

Response: members: The members of the team after the change. message: Confirmation.

GET /api/v1/teams/<team_id>/message-templates

List the canned messages of a team (any member of the team, agents included).

Access: team access (team_id)

Path parameters

Name Description
team_id Team ID.

Response: message_templates: List of {id, title, body}.

POST /api/v1/teams/<team_id>/webhook/regenerate

Regenerate team webhook secret.

Open to the managers and admins of the team's organization (permission configure_provider, like the provider settings): they already read the secret in the team list, and the previous URL stops working at once, so the provider must be updated with the new one.

Access: team access (team_id); permission configure_provider (now teams.provider)

Path parameters

Name Description
team_id Team ID.

Response: webhook_secret: The new secret (the old URL stops working). webhook_url: The new inbound webhook path, /webhook/sms/<secret> (prefix it with the API address).

GET /api/v1/teams/all-organizations

List all teams from all organizations the user belongs to.

This is used for WebSocket room joining to receive real-time updates from all organizations. While the header X-Admin-Organization is honored, only the teams of the administered organization are listed.

Access: authenticated (JWT or API key)

Query parameters

Name Description
member_only If "true", only the teams of the member view of the mobile app (by the profiles held in each organization: with teams.all every team, otherwise only the team they are placed in, nothing without one; a platform permission adds nothing); the mobile app asks for those.

Response: team_ids: Ids of all the teams of the organization. total: Number of teams.

SMS & conversations

Sending SMS/MMS, conversations, archives.

GET /api/v1/conversations

List conversations for user's teams.

Conversations come from the most recent message to the oldest (last_message_at), open and closed ones (since 2026-10-14 closing a conversation is archiving it: there is no separate archived state, and the former archived conversations became closed ones). The closed conversations of a team by day of closing are also GET /api/v1/teams/<team_id>/archives, which requires the archives permission messages.archive (Admin and Manager profiles; refused to a restricted API key).

Access: permission view_team_conversations (now messages.read)

Query parameters

Name Description
team_id Team ID (optional, if not provided loads all user's teams). 403 forbidden for a team the user (or the API key) cannot access.
status open or closed (optional, default: both). Any other value, archived included, is refused (400 validation_error, field status).
replied 1 = only conversations the contact wrote to at least once, 0 = only those without any inbound message yet (optional).
q Free-text search (optional, 100 characters at most). Digits and phone punctuation (514 555, (514) 555-0123) are looked for in the contact's number and in the team number of the conversation; any other text, without regard to case and accents, in the contact's name and in the messages of the conversation.
number Only the conversations that use this team number (optional): an E.164 number in any usual notation, or the ID of a phone number. 400 validation_error (field number) for anything else.
unread 1 = only the conversations with unread messages (optional, 1/0 or true/false).
member_only 1 = only the teams the user works in, by the profiles of each membership: with teams.all (Admin, Manager) every team of that organization, otherwise only the team they are placed in, nothing without one; a platform permission adds nothing (the web app administers). The mobile app asks for it (optional, 1/0 or true/false). With team_id, a team outside this view is refused (403 forbidden).
skip Pagination offset (default 0; a negative value counts as 0, a value over 2147483647 is refused: 400 validation_error).
limit Page size (default 50, max 100; a larger value is brought back to 100, 0 or less to 1). A skip or limit that is not an integer is refused (400 validation_error).

Response: conversations: The page of conversations, each with its team_name and last_message ({id, body, direction, status, created_at, has_media}, null for a conversation without message). total: Number of conversations matching ALL the filters, whatever the page. skip, limit: The values applied.

DELETE /api/v1/conversations/<conversation_id>

Delete a conversation for good, with its messages, notes and timeline.

Open to the admins of the organization (permission delete_conversations: admin, domain admin, super admin); managers and agents get 403 forbidden. The role is the one held in the organization that owns the conversation, not the role in the organization the caller is currently working in. The unread counters of the team's users are announced (unread_update).

Access: permission delete_conversations (now messages.delete)

Path parameters

Name Description
conversation_id Conversation ID.

Response: Success message.

GET /api/v1/conversations/<conversation_id>

Get conversation details with messages.

Reading it marks it as read (conversation.updated with change "read" when it had unread messages). A closed conversation is returned like any other to whoever reaches its team: the archives permission only guards the list of GET /api/v1/teams/<team_id>/archives.

Access: permission view_team_conversations (now messages.read)

Path parameters

Name Description
conversation_id Conversation ID.

Query parameters

Name Description
skip Message pagination offset (default 0; a negative value counts as 0, a value over 2147483647 is refused: 400 validation_error).
limit Number of messages (default 50, max 100; a larger value is brought back to 100, 0 or less to 1). A skip or limit that is not an integer is refused (400 validation_error).
mark_read 0 reads the conversation without marking it read (support, administration; default 1). The conversation then keeps its unread count and nothing is announced. Another value than 1/0/true/false is refused (400 validation_error).

Response: conversation: The conversation. messages: The messages of the page, newest first (skip/limit). total: Number of messages in the conversation. capabilities: What the number of the conversation can carry, a list such as ["SMS", "MMS"]. events: Every status change of the conversation in timeline order (type closed / reopened, cause inbound / agent / outbound, actor_name, via, message_id of the inbound message that reopened it, anchor_at).

PATCH /api/v1/conversations/<conversation_id>

Name the contact of a conversation, or clear the name.

Whoever may write in the conversation may rename it: the same rule as sending a message to it with POST /api/v1/sms/send (permission send_sms and access to the team, the member view of the mobile app included); an integration key whose exposed teams include the team of the conversation too. The name belongs to this conversation only (there is no shared address book: another conversation with the same number, through another team number or another team, keeps its own name). It is the name shown in the lists and searched by the q filter of GET /api/v1/conversations. An open or a closed conversation can be renamed; nothing reopens.

A real change is announced to the team (socket conversation_update with change and type "renamed" and contact_name) and delivered to the integrations as conversation.updated with change "renamed" and contact_name. The same name again changes and announces nothing.

Access: permission send_sms (now messages.send)

Path parameters

Name Description
conversation_id Conversation ID.

Request body

Name Description
contact_name The contact's name (required, may be null): trimmed, then 1 to 120 characters on one line, without control characters; null or an empty string (also once trimmed) clears the name.

Response: conversation: The conversation, in the form of GET /api/v1/conversations/<id> (contact_name updated).

Errors 400 validation_error (field contact_name): body not a JSON object, contact_name missing, not a string nor null, longer than 120 characters, or carrying a line break or a control character. 403 forbidden: the team of the conversation is not accessible. 404 not_found: unknown conversation.

POST /api/v1/conversations/<conversation_id>/archive

Archive a conversation: since 2026-10-14 the same as closing it (POST .../close), kept for the clients that still call it. A closed conversation IS archived: it leaves the list of the open ones, shows in the team's closed conversations (GET /teams/<team_id>/archives) and reopens when the contact writes. Delivers conversation.updated with change "closed" and cause "agent".

Access: permission archives (now messages.archive)

Path parameters

Name Description
conversation_id Conversation ID.

Response: conversation: The closed conversation. message: Confirmation.

POST /api/v1/conversations/<conversation_id>/close

Close a conversation.

Delivers conversation.updated with change "closed" and cause "agent".

Access: permission view_team_conversations (now messages.read)

Path parameters

Name Description
conversation_id Conversation ID.

Request body

Name Description
actor_name Integration keys only: name of the person acting through the integration (shown in the timeline), 120 characters max.

Response: conversation: The updated conversation. message: Confirmation text.

POST /api/v1/conversations/<conversation_id>/read

Mark a conversation as read, explicitly (the other devices of the user follow).

Reading it with GET /api/v1/conversations/<id> marks it too (unless mark_read=0). When the conversation had unread messages, conversation.updated with change "read" and the unread_update counters are announced; marking an already read conversation changes and announces nothing.

Access: permission view_team_conversations (now messages.read)

Path parameters

Name Description
conversation_id Conversation ID.

Response: conversation: The conversation (unread_count 0).

Errors 403 forbidden: the team is not accessible. 404 not_found: unknown conversation.

POST /api/v1/conversations/<conversation_id>/reopen

Reopen a closed conversation.

Delivers conversation.updated with change "reopened" and cause "agent". A conversation also reopens by itself, with the same event: cause "inbound" when the contact writes to a closed conversation (message_id then gives that message). Sending a message in a closed conversation leaves it closed (before 2026-10-14, a message sent in an archived one reopened it, cause "outbound").

Access: permission view_team_conversations (now messages.read)

Path parameters

Name Description
conversation_id Conversation ID.

Request body

Name Description
actor_name Integration keys only: name of the person acting through the integration (shown in the timeline), 120 characters max.

Response: conversation: The updated conversation.

GET /api/v1/conversations/unread-count

Unread counters of the menu: the same numbers as the unread_update event.

Counts the conversations with unread messages and the unread messages of every team the user can read in the organization they work in (the administered organization in the administration context), closed conversations included.

Access: permission view_team_conversations (now messages.read)

Query parameters

Name Description
number Only the conversations that use this team number (optional): an E.164 number in any usual notation, or the ID of a phone number. 400 validation_error (field number) for anything else.
member_only 1 = only the teams of the member view of the mobile app (optional, same rule as GET /conversations).

Response: unread: {conversations: number of conversations with unread messages, messages: number of unread messages}.

GET /api/v1/messages

List messages with filters.

Messages of one team, the most recent first, those of closed conversations included.

Access: permission view_team_conversations (now messages.read)

Query parameters

Name Description
team_id Team ID (optional when the user has a team; an integration key must give it). 403 forbidden otherwise, or for a team that is not accessible.
direction Filter by direction (inbound, outbound).
status Filter by status.
skip Pagination offset (default 0; a negative value counts as 0, a value over 2147483647 is refused: 400 validation_error).
limit Page size (default 50, max 100; a larger value is brought back to 100, 0 or less to 1). A skip or limit that is not an integer is refused (400 validation_error).

Response: messages: The page of messages. skip, limit: The values applied.

DELETE /api/v1/messages/<message_id>

Delete one message for good (admins of the organization that owns the message, like deleting a conversation).

The conversation stays, even when it is left without message; its unread count is brought down to the number of inbound messages left, and the unread counters of the team's users are announced (unread_update). Files attached to the message are not removed.

Access: permission delete_conversations (now messages.delete)

Path parameters

Name Description
message_id Message ID.

Response: Success message.

Errors 403 forbidden: the team of the message is not accessible, or the role is not admin. 404 not_found: unknown message.

POST /api/v1/sms/send

Send an SMS or MMS message.

The message is recorded in the conversation of the team number and the contact (created if needed; a closed one stays closed: since 2026-10-14 only a message from the contact reopens it), then handed to the provider. Subscribed integrations receive it as message.sent.

Access: permission send_sms (now messages.send)
Rate limit: 100 per hour (600 per hour for an integration key)

Request body

Name Description
to Recipient phone number (required).
body Message text (required unless media_urls is given).
team_id Team ID (optional when the user has a team, or when from_number identifies the team). An integration key has no team of its own: pass team_id or from_number (403 forbidden otherwise).
from_number Sender phone number (optional, uses the number of the contact's conversation or the first team number). Without team_id, the team the number is assigned to is used. 400 number_not_in_team when it is not a number of the team.
media_urls MMS attachments (optional): a list of files uploaded with POST /api/v1/upload, each given as the returned url (a string) or as an object {url, content_type}. Any other URL, or a file that no longer exists, is refused (400 validation_error). The content type recorded with the message is the uploaded file's.
sender_name Integration keys only: name of the person sending through the integration, shown as the sender (default: the integration name), 120 characters max.
external_sender Integration keys only: the integration's own identifier of that person, 120 characters max.

Response: message: The created message. conversation: Its conversation (201).

Errors 403 number_disabled: the number that would send is switched off (reason: paused, with paused_reason monthly_fee or balance for a pause of the platform; inactive; sms_disabled), whether it was asked for with from_number or it is the only one of the team; a switched-off number is skipped when the sender is chosen by the platform. 400 validation_error: body not a JSON object, to missing or not a phone number (field to: a number in E.164 or a valid North American number, or a short code of 3 to 9 digits), neither body nor media_urls, body not a string, body longer than the limit of the domain (field body: 800 characters by default, setting settings.sms.max_body_length of the domain, 0 for no limit), media_urls not a list of uploads. 400 number_not_in_team: from_number is not (or no longer) a number of the team. 400 configuration_error: no number assigned to the team, provider not configured or not enabled, domain not found. 402 insufficient_balance: the organization's credit does not cover the message (estimated_cost and current_balance in the answer); the message is debited BEFORE the provider is asked, so requests sent at the same time cannot share one message's worth of credit (the loser gets this answer and its message is recorded as failed, error_code insufficient_balance), and a message the provider does not send is credited back. 402 account_suspended: the balance stayed negative past the grace period (E46); balance_state, release_at and balance in the answer; sending resumes when the balance is back to zero or more. 403 forbidden: team not accessible, or no team given or found. 404 not_found: team. 429: rate limit. 500 send_failed: the provider refused or failed (or, error_code billing_error, the debit itself failed and nothing was sent); error_code and message_id give the message, recorded as failed in the conversation; no message.sent is delivered, the debit is credited back, and a retry creates a new message.

GET /api/v1/teams/<team_id>/archives

The closed conversations of a team (its « archives »: since 2026-10-14 a closed conversation is archived, there is no separate archived state), most recently closed first, by the date of the closing (closed_at). The list of the conversations gives them too with status=closed.

Access: permission archives (now messages.archive)

Path parameters

Name Description
team_id Team ID.

Query parameters

Name Description
date Optional date filter: closed that day (YYYY-MM-DD format, in the team's time zone).
skip Pagination offset (default 0; a negative value counts as 0, a value over 2147483647 is refused: 400 validation_error).
limit Page size (default 50, max 100; a larger value is brought back to 100, 0 or less to 1). A skip or limit that is not an integer is refused (400 validation_error).

Response: conversations: The closed conversations (each with closed_at). skip, limit: The values applied.

GET /api/v1/teams/<team_id>/archives/dates

The days on which conversations of the team were closed (its « archives »).

Access: permission archives (now messages.archive)

Path parameters

Name Description
team_id Team ID.

Response: List of dates (YYYY-MM-DD format).

GET /api/v1/teams/<team_id>/phone-numbers

Get phone numbers assigned to a team.

Access: permission send_sms (now messages.send)

Path parameters

Name Description
team_id Team ID.

Response: phone_numbers: List of {number, label, capabilities} (active numbers of the team; capabilities = ["SMS"] or ["SMS", "MMS"], empty for a number that no SMS provider carries).

Conversation notes

Internal notes of a conversation (never sent to the contact).

GET /api/v1/conversations/<conversation_id>/notes

List the internal notes of a conversation.

Notes are silent comments of the team: they are never sent to the contact. They come in timeline order (anchor_at): the moment they were written, or the time of the message they comment (message_id).

Access: permission view_team_conversations (now messages.read)

Path parameters

Name Description
conversation_id Conversation ID.

Query parameters

Name Description
skip Number of notes to skip (optional, default 0; a negative value counts as 0, a value over 2147483647 is refused: 400 validation_error).
limit Number of notes (optional, default: every note; max 100, a larger value is brought back to 100, 0 or less to 1). A skip or limit that is not an integer is refused (400 validation_error).

Response: notes: Notes of the conversation, in timeline order (id, conversation_id, team_id, message_id, anchor_at, body, author_id, author_name, via, external_author, created_at, updated_at). total: Number of notes of the conversation.

POST /api/v1/conversations/<conversation_id>/notes

Add an internal note to a conversation (never sent to the contact).

Delivers conversation.note with change "created" to the integrations.

Access: permission view_team_conversations (now messages.read)

Path parameters

Name Description
conversation_id Conversation ID.

Request body

Name Description
body Note text (required, 4000 characters max after trimming spaces): 400 validation_error when missing, empty or longer.
message_id Message of the conversation the note comments (optional): the note is placed right after it. 404 not_found when it is not a message of this conversation.
author_name Integration keys only: name of the person writing through the integration (default: the integration name), 120 characters max.
external_author Integration keys only: the integration's own identifier of that person, 120 characters max.

Response: note: The created note (201), with the fields of GET .../notes.

DELETE /api/v1/conversations/<conversation_id>/notes/<note_id>

Delete an internal note (its author, or a member whose profiles in the organization of the conversation give members.manage or messages.delete: the Admin and Manager profiles; or a platform or domain administrator). A restricted API key deletes only its owner's notes.

Access: permission view_team_conversations (now messages.read)

Path parameters

Name Description
conversation_id Conversation ID.
note_id Note ID.

Response: success: True.

PUT /api/v1/conversations/<conversation_id>/notes/<note_id>

Edit the text of an internal note (its author only).

An integration key edits the notes written through it; the integration checks external_author on its side. Delivers conversation.note with change "updated" to the integrations.

Access: permission view_team_conversations (now messages.read)

Path parameters

Name Description
conversation_id Conversation ID.
note_id Note ID.

Request body

Name Description
body New note text (required, 4000 characters max after trimming spaces): 400 validation_error when missing, empty or longer.

Response: note: The updated note, with the fields of GET .../notes. 403 forbidden for a note of another author, 404 not_found for an unknown or deleted note.

Phone numbers

Phone number registry and team assignment.

GET /api/v1/phone-numbers

List the phone numbers of the current organization (E09).

With an API key, only the numbers assigned to the teams the key reaches are listed (a number without a team is left out). A restricted key (limited to a team, or an integration's key) gets the numbers of its teams, whatever organization its owner currently works in.

Access: authenticated (JWT or API key)

Query parameters

Name Description
status active, paused, released (the trash of the organization, E11) or pending (order not yet delivered); absent = active, paused and pending.
q Text searched in the label and in the digits of the number.
skip Pagination offset (default 0).
limit Page size (default 50, at most 100).

Response: phone_numbers: The page of numbers (registry fields, plus usage, use_label_as_caller_id, state {status, enabled, sleep, released_at, expires_at}, features {voicemail, recording, sms {available, enabled}}, routing, type, area_code, city, region, order, team_name). total: Number of numbers matching the filters. skip: Offset applied. limit: Page size applied. counts: Numbers by status after the text filter: {active, paused, released, pending}.

GET /api/v1/phone-numbers/<phone_id>

Get phone number details.

With an API key, only a number assigned to a team the key reaches (403 otherwise); for a restricted key, whatever organization its owner currently works in.

Access: authenticated (JWT or API key)

Path parameters

Name Description
phone_id Phone number ID.

Response: phone_number: The number.

PATCH /api/v1/phone-numbers/<phone_id>

Change the attributes of a number of the organization (the fields given change).

Not available to API keys. Errors are {error, message}: 400 validation_error with field (label longer than 24 characters, usage, sms_enabled on a number without SMS or without a team to put it in), 403, 404, 409 conflict (number in the trash).

Access: permission numbers.manage in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
phone_id Phone number ID.

Request body

Name Description
label Name of the number, at most 24 characters, no control character or line break (empty = the number itself).
usage business or personal.
sms_enabled true puts the number in the default team of the organization (when it has none) and switches SMS on; false takes it out of its team.

Response: phone_number: The number after the change (same object as GET /phone-numbers/<id>).

PUT /api/v1/phone-numbers/<phone_id>

Switch a phone number on or off (is_active), for the platform only.

An inactive number is not charged the monthly fees, so only the platform permissions (platform_admin, domain_admin, acting for the organization with the X-Admin-Organization header) may write is_active; any other member, even with numbers.manage, is refused (403 forbidden) and uses PATCH /phone-numbers/<id> (sms_enabled, label, usage) or releases the number instead. The number must belong to the current organization (403 forbidden otherwise). A request without is_active changes nothing.

Access: permission numbers.manage in the organization (profiles)

Path parameters

Name Description
phone_id Phone number ID.

Request body

Name Description
is_active Whether number is active, a boolean (400 validation_error, field is_active, otherwise).

Response: phone_number: The updated number. message: Confirmation.

POST /api/v1/phone-numbers/<phone_id>/assign

Assign phone number to a team.

Access: permission numbers.manage in the organization (profiles)

Path parameters

Name Description
phone_id Phone number ID.

Request body

Name Description
team_id Team ID to assign (null to unassign).

Response: phone_number: The number after the change. message: Confirmation (assigned, or unassigned when team_id is null).

GET /api/v1/phone-numbers/<phone_id>/prices

Prices of a number of the organization: installation, monthly fee, options, forwarding per minute.

Amounts come from the pricing plan of the organization (rates did_setup, did_monthly, feature_, voice_minute_forward), the number's own prices winning for the installation and the monthly fee. Only exposes prices: the monthly billing is separate. Not available to API keys.

Access: authenticated (JWT or API key)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
phone_id Phone number ID.

Response: prices: {currency, setup, monthly, features {voicemail, recording, sms, flow}, per_minute {forward}}.

GET /api/v1/phone-numbers/<phone_id>/provider

Get SMS provider info for a phone number (read-only: the domain administrator manages the provider).

Access: permission numbers.manage in the organization (profiles)

Path parameters

Name Description
phone_id Phone number ID.

Response: provider: The provider name (the configuration is managed by the domain administrator). message: A reminder that the domain administrator manages it.

POST /api/v1/phone-numbers/<phone_id>/release

Release a number: it goes to the trash of the organization (E11).

The number leaves the organization (no team, no billing, no calls) but stays RESERVED for it until state.expires_at (the domain's trash_days, 10 days by default; 0 = given back to the domain pool at once), and can be restored until then. Its routing is switched off, not deleted. Not available to API keys. Errors are {error, message}: 403, 404, 409 conflict (already in the trash, access number, order not delivered).

Access: permission numbers.manage in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
phone_id Phone number ID.

Response: phone_number: The number, with state.status released, state.released_at and state.expires_at.

POST /api/v1/phone-numbers/<phone_id>/restore

Take a number back out of the trash (E11).

The number returns to the organization with its label, usage, voice settings and routing, and to its team (the default team when that team is gone). Nothing is charged here. Not available to API keys. Errors are {error, message}: 402 insufficient_balance (with balance and required: the balance does not cover the monthly fee of the number), 403, 404 (not in the trash of this organization), 409 expired (the reservation has ended) or conflict.

Access: permission numbers.manage in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
phone_id Phone number ID.

Response: phone_number: The restored number.

GET /api/v1/phone-numbers/unassigned

List unassigned phone numbers.

Not available to restricted API keys (a key limited to a team, or an integration's key): 403 with error forbidden.

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: phone_numbers: The numbers of the organization that no team uses. total: Number of numbers.

Number purchase

Search of available numbers, prices and purchase (permission numbers.buy).

POST /api/v1/phone-numbers/acquire

Buy a number: installation fee and first month are debited from the credits.

Either a number from the search results, or criteria (the first match is bought). The number goes to the default team of the organization with SMS on. Two transactions (installation, first month) appear in the history. A number ordered from a provider that is not delivered at once comes back with 202 and pending true: it is delivered later (or refunded and removed if the provider fails). Not available to API keys. Errors are {error, message}: 400 validation_error / country_unsupported, 402 insufficient_balance (with balance and required, nothing is charged), 409 number_unavailable, 502/503 provider errors.

Access: permission numbers.buy in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Request body

Name Description
number Number from the search results (E.164); or
country Country (required without number).
type local or tollfree.
area_code Area code.
city City.
state Province or state.

Response: phone_number: The number bought (same object as GET /phone-numbers/<id>). balance: Credit balance after the debit. pending: True while the order awaits the provider's delivery.

GET /api/v1/phone-numbers/available

Search the numbers that can be bought, with their prices for the organization.

Sources: the domain pool first, then (when the domain uses a number provider) its API if the pool has nothing that matches. Not available to API keys. Errors are {error, message}: 400 validation_error with field, 502/503 provider_error / provider_unavailable.

Access: permission numbers.buy in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Query parameters

Name Description
country ISO 3166-1 alpha-2 country, required (CA, US...).
type local or tollfree.
state Province or state.
city City (part of the name).
area_code Area code (3 digits).
skip Offset (default 0).
limit Page size (default 25, at most 25).

Response: results: List of {number, type, country, state, city, area_code, setup_fee, price_per_month, currency, capabilities}. count: Number of numbers matching (all pages).

GET /api/v1/phone-numbers/available/countries

Countries and number types the domain sells (pool and number provider merged).

Not available to API keys. Errors are {error, message}: 502/503 when the number provider fails and the pool has nothing.

Access: permission numbers.buy in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Response: countries: List of {code, name, types} where types are local and/or tollfree.

GET /api/v1/phone-numbers/available/regions

Provinces and cities in which numbers can be bought (free numbers of the pool and the number provider), to fill the state and city of GET /available.

A number provider that sells by region (the platform's today) answers only a search that names a province, a city or an area code. Not available to API keys. Errors are {error, message}: 502/503 when the number provider fails and the pool has nothing.

Access: permission numbers.buy in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Response: regions: [{code (province or state), cities: [name, ...]}], sorted.

Number porting

Bring numbers from another carrier: check, request, follow-up, correction after a refusal, cancellation (permission numbers.buy, when the domain allows it).

POST /api/v1/phone-numbers/port/check

Check whether numbers can be ported in, before a request.

Not available to API keys. Errors are {error, message}: 403 port_not_enabled, 502/503 when the provider fails.

Access: permission numbers.buy in the organization (profiles)

Request body

Name Description
numbers 1 to 50 Canadian numbers (required).

Response: results: [{number, portable, reason (already_here: the number is already on the platform, in_progress: another request has it, not_portable), carrier (current carrier), rate_center}].

GET /api/v1/phone-numbers/port/config

Whether the organization may port numbers in, the earliest date it may ask and what a number costs.

Not available to API keys.

Access: permission numbers.buy in the organization (profiles)

Response: enabled: The domain lets its organizations port their numbers in. earliest_due_date: The first date a request may ask (YYYY-MM-DD; the carriers need 5 business days). provinces: Provinces of the service address. max_numbers: Numbers in one request at most. fees: {port_fee, monthly, currency}: what one number costs, debited when its port completes (null when not enabled). balance: Credit balance of the organization.

GET /api/v1/port-requests

The port requests of the organization, newest first.

Not available to API keys.

Access: permission numbers.buy in the organization (profiles)

Query parameters

Name Description
state open (every request not completed nor cancelled) or a state (submitted, carrier_pending, confirmed, rejected, cancel_requested, cancelled, completed).
skip Offset (default 0).
limit Page size (default 50, at most 100).

Response: port_requests: List of requests (as GET /port-requests/<id>, without events). total: Requests matching. skip, limit: The page.

POST /api/v1/port-requests

Ask to port numbers in: they move to the organization when the current carrier lets them go.

The request goes to the carriers at once. Nothing is debited now: the fee of the port and the first month of each number are debited when it arrives; the balance must cover them when asking. Follow it with GET /port-requests/<id> (the event port_update tells each change in real time; an e-mail is sent when it is confirmed, refused, completed or cancelled). Not available to API keys. Errors are {error, message}: 400 validation_error with field, 402 insufficient_balance (with balance and required), 403 port_not_enabled, 409 number_not_portable (with numbers: the results of the check that failed), 422 port_refused (the provider refused the request), 502/503.

Access: permission numbers.buy in the organization (profiles)

Request body

Name Description
numbers 1 to 50 Canadian numbers, held by the same holder (required).
full_name Name (person or company) on the account at the current carrier (required).
street_number Civic number of the service address (required).
street_name Street of the service address (required).
floor Floor.
suite Suite.
city City (required).
province Province code: AB, BC, MB, NB, NL, NS, NT, NU, ON, PE, QC, SK or YT (required).
postal_code Canadian postal code (required).
account_number Account number at the current carrier (required).
current_carrier Name of the current carrier.
due_date Wanted date, YYYY-MM-DD, a weekday from earliest_due_date of GET /phone-numbers/port/config (required).
comments Note for the carriers (up to 500 characters).

Response: port_request: The request (as GET /port-requests/<id>).

GET /api/v1/port-requests/<request_id>

A port request and its history.

Not available to API keys.

Access: permission numbers.buy in the organization (profiles)

Path parameters

Name Description
request_id Port request ID.

Response: port_request: {id, state (submitted, carrier_pending: at the current carrier, confirmed: date set, rejected: to correct and send again, cancel_requested, cancelled, completed), scope (organization or pool), organization_id, organization_name, domain_id, numbers [{number, state (pending, carrier_pending, confirmed, rejected, cancelled, ported), carrier, rate_center, due_date (confirmed date), rejection [{code, message, remarks, hint}], phone_id (once ported), ported_at}], holder {full_name, street_number, street_name, floor, suite, city, province, postal_code, account_number, current_carrier}, due_date, confirmed_date, comments, fees {port_fee, monthly, currency}, rejection_hint (holder_mismatch, not_portable, conflicting_order, due_date or other), attention {at, number, message} (the request changed at the carrier), cancel_request {at, reason, refused_at, refused_reason}, attempts, submit_uncertain, created_at, updated_at, completed_at, events [{kind (request or provider), event, number, due_date, at, rejection, remarks, reason}]}.

POST /api/v1/port-requests/<request_id>/cancel

Cancel a port request that is not completed.

When the provider cannot cancel by itself, the request becomes cancel_requested: the platform asks the provider and closes it (or tells why it could not: the port goes on). Not available to API keys. Errors are {error, message}: 409 conflict (completed, cancelled or already asked).

Access: permission numbers.buy in the organization (profiles)

Path parameters

Name Description
request_id Port request ID.

Request body

Name Description
reason Why (up to 300 characters).

Response: port_request: The request (as GET /port-requests/<id>).

POST /api/v1/port-requests/<request_id>/resubmit

Send a refused request again, corrected. The fields given change; the others are kept.

Not available to API keys. Errors are {error, message}: 400 validation_error, 409 conflict (the request is not refused), 422 port_refused, 502/503.

Access: permission numbers.buy in the organization (profiles)

Path parameters

Name Description
request_id Port request ID.

Request body

Name Description
full_name As POST /port-requests.
street_number As POST /port-requests.
street_name As POST /port-requests.
floor As POST /port-requests.
suite As POST /port-requests.
city As POST /port-requests.
province As POST /port-requests.
postal_code As POST /port-requests.
account_number As POST /port-requests.
current_carrier As POST /port-requests.
due_date As POST /port-requests.
comments As POST /port-requests.

Response: port_request: The request (as GET /port-requests/<id>).

Number porting (domain)

The port requests of the domain's organizations, and porting numbers into the domain's pool (domain admin).

GET /api/v1/domains/<domain_id>/port-config

Whether the domain can port numbers into its pool, from when, at what cost.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: enabled: A number provider of the domain ports numbers in. organizations_enabled: The organizations may port their own numbers (port_in_enabled of PUT /domains/<id>/did-provider). earliest_due_date, provinces, max_numbers: As GET /phone-numbers/port/config. fees: {port_fee, monthly, currency}: the provider's cost of one number ported into the pool.

GET /api/v1/domains/<domain_id>/port-requests

Every port request of the domain: its organizations' and its own (into the pool), newest first.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Query parameters

Name Description
state open or a state (as GET /port-requests).
scope organization or pool.
organization_id Only this organization.
skip Offset (default 0).
limit Page size (default 50, at most 100).

Response: port_requests: As GET /port-requests. total, skip, limit: The page.

POST /api/v1/domains/<domain_id>/port-requests

Port numbers into the pool of the domain (they arrive free, to sell or assign). Journalled.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
numbers As POST /port-requests.
full_name As POST /port-requests.
street_number As POST /port-requests.
street_name As POST /port-requests.
floor As POST /port-requests.
suite As POST /port-requests.
city As POST /port-requests.
province As POST /port-requests.
postal_code As POST /port-requests.
account_number As POST /port-requests.
current_carrier As POST /port-requests.
due_date As POST /port-requests.
comments As POST /port-requests.

Response: port_request: The request (as GET /port-requests/<id>).

Errors 400 validation_error. 404 port_unavailable (no number provider of the domain ports numbers in). 409 number_not_portable. 422 port_refused. 502/503.

GET /api/v1/domains/<domain_id>/port-requests/<request_id>

A port request of the domain and its history (as GET /port-requests/<id>).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
request_id Port request ID.

Response: port_request: As GET /port-requests/<id>.

POST /api/v1/domains/<domain_id>/port-requests/<request_id>/cancel

Cancel a port request of the domain (as POST /port-requests/<id>/cancel).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
request_id Port request ID.

Request body

Name Description
reason Why (up to 300 characters).

Response: port_request: As GET /port-requests/<id>.

POST /api/v1/domains/<domain_id>/port-requests/<request_id>/resubmit

Send a refused request of the domain again, corrected (as POST /port-requests/<id>/resubmit).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
request_id Port request ID.

Request body

Name Description
full_name As POST /port-requests.
street_number As POST /port-requests.
street_name As POST /port-requests.
floor As POST /port-requests.
suite As POST /port-requests.
city As POST /port-requests.
province As POST /port-requests.
postal_code As POST /port-requests.
account_number As POST /port-requests.
current_carrier As POST /port-requests.
due_date As POST /port-requests.
comments As POST /port-requests.

Response: port_request: As GET /port-requests/<id>.

POST /api/v1/domains/<domain_id>/port-requests/check

Check whether numbers can be ported in (as POST /phone-numbers/port/check).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
numbers 1 to 50 Canadian numbers (required).

Response: results: As POST /phone-numbers/port/check.

Number pool and provider

Numbers the domain sells (one by one, in batch, or bought from the number provider), where the numbers the organizations buy come from, trash (domain admin).

GET /api/v1/domains/<domain_id>/did-provider

Where the numbers the organizations buy come from, the number providers the domain may use, and the trash settings.

The number providers are configured by the platform (GET /api/v1/system/number-providers); a domain admin only sees each one's id and description, never its name.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: did_provider: {type (pool, provider or hybrid), account_id (the number provider chosen; null = the first one available), countries, pricing {mode (plan or cost_plus), setup_markup_percent, monthly_markup_percent}, import_unknown_stock, port_in_enabled, webhook_secret (masked), api_configured (a number provider is available)}. providers: The number providers the domain may use: [{id, description, capabilities {prices, release, tollfree_order, regions, sms_toggle, synchronous_orders, port_in, port_cancel}}] (name only for a super admin). numbers: {trash_days, release_to_provider, default_voice_trunk_id}.

PUT /api/v1/domains/<domain_id>/did-provider

Change where the numbers the organizations buy come from (the fields given change).

Switching back to pool changes nothing for the numbers already bought. Journalled.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
type pool (default: the organizations buy the numbers of the pool), provider (they buy straight from the number provider), or hybrid (pool first, the provider when the pool has nothing that matches). provider and hybrid need a number provider available for the domain (400 validation_error otherwise).
account_id The number provider to use (an id of providers in GET); null = the first one available.
countries Countries offered.
pricing {mode: plan (price list of the pricing plan) or cost_plus (provider cost plus markup), setup_markup_percent, monthly_markup_percent}.
import_unknown_stock Add to the pool the numbers the provider holds for the domain and we do not know (default false).
port_in_enabled The organizations may port their own numbers in, through the domain's number provider (default false; the domain admin can always port numbers into the pool).
webhook_secret Secret in the delivery webhook URL /webhook/numbers/<webhook_secret>, for a provider that delivers its orders later: 16 to 128 characters among letters, digits, - and _.
trash_days Days a released number stays reserved for its organization, 0 to 365 (default 10; 0 = given back at once).
release_to_provider Give a number bought from the provider back to it when it returns to the pool, when the provider can take it back (default true).
default_voice_trunk_id Voice trunk of the domain given to the numbers customers acquire (they then reach the voice service: voice_enabled); null leaves the voice to the domain admin, number by number.

Response: did_provider: The settings after the change (as GET). providers: As GET. numbers: {trash_days, release_to_provider, default_voice_trunk_id}.

POST /api/v1/domains/<domain_id>/did-provider/sync

Follow the number provider now (the periodic task does the same).

Follows the pending orders (delivery or refund), retries the releases that failed and reconciles the stock the provider holds with the numbers we know (missing and unknown numbers are reported; unknown ones are added to the pool only when import_unknown_stock is on).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
stock false skips the stock reconciliation (default true).
import_unknown true or false overrides import_unknown_stock for this run.

Response: result: {orders {followed, delivered, failed, pending}, stuck, releases {retried, released, failed}, stock ({provider_count, our_count, missing_at_provider, unknown_at_provider, imported} or null), error ({code, message} or null)}.

POST /api/v1/domains/<domain_id>/did-provider/test

Check the connection to the number provider of the domain (one cheap read call).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: ok: Whether the provider answered and accepted the credentials. message: What happened. provider: "provider" when a number provider was tested, "pool" when the domain has none.

GET /api/v1/domains/<domain_id>/number-market/available

Numbers the number provider sells now, at its cost, to buy them for the pool.

The provider is asked live: a number shown may be taken by someone else before it is ordered. Numbers we already hold are left out. At least a province, a city or an area code is needed with a provider that sells by region (an empty list otherwise).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Query parameters

Name Description
state Province (code, e.g. QC).
city City (rate center), its start is enough, without case nor accents.
area_code Area code (3 digits) or area code and exchange (6 digits).
account_id The number provider; default: the domain's.
limit Numbers returned, 1 to 25 (default 25).

Response: results: [{number, type, country, state, city, area_code, setup_cost, monthly_cost, currency, capabilities}]. count: Numbers found.

Errors 404 provider_not_configured. 502/503: the provider failed.

POST /api/v1/domains/<domain_id>/number-market/order

Buy numbers from the number provider into the pool of the domain.

Each number is ordered on its own; the ones the provider delivers enter the pool (status free), bound to the provider's SMS account (SMS turned on at the provider) and to its voice trunk when the domain may use it, at the provider's cost (setup and monthly cost recorded on the number). Nothing is debited here: the domain then sells them to its organizations or assigns them. Journalled.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
Rate limit: 30 per hour

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
numbers 1 to 10 E.164 numbers from GET .../number-market/available (required).
account_id The number provider; default: the domain's.

Response: results: [{number, status: ordered (in the pool), pending (the provider delivers later), unavailable (taken meanwhile, or already ours) or failed, phone_id, message}]. ordered: How many entered the pool.

Errors 400 validation_error. 404 provider_not_configured.

GET /api/v1/domains/<domain_id>/number-market/regions

Provinces and cities in which the number provider sells numbers (to search them).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Query parameters

Name Description
account_id The number provider (an id of providers in GET /domains/<id>/did-provider); default: the domain's.

Response: regions: [{code (province), cities: [name, ...]}], sorted.

Errors 404 provider_not_configured: no number provider for the domain. 502/503: the provider failed.

GET /api/v1/domains/<domain_id>/number-pool

The numbers of the domain with their pool status and prices.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Query parameters

Name Description
status free, assigned, trash, held, pending, returned or access; absent = all.
country Only this country.
type local or tollfree.
q Text searched in the number, the label and the city.
skip Pagination offset (default 0).
limit Page size (default 50, at most 100).

Response: numbers: The page (registry fields plus pool_status, source, type, area_code, city, region, country, capabilities, pricing {setup_fee, monthly_fee} (null = the pricing plan decides), organization_name, released_at, expires_at). total: Number matching the filters. skip: Offset applied. limit: Page size applied. counts: Numbers by pool status, whatever the filters.

POST /api/v1/domains/<domain_id>/number-pool

Add one number to the pool of the domain.

Errors are {error, message}: 400 validation_error with field (not an E.164 number, bad attribute), 409 conflict (the number already exists, in this domain or another).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
number Phone number (E.164, or North American digits).
country ISO 3166-1 alpha-2 country (deduced for +1 numbers).
type local or tollfree (deduced from the area code).
area_code Area code (deduced for +1 numbers).
city City.
region Province or state.
capabilities List of voice, sms (both by default).
setup_fee Installation price charged to the buyer (null or absent = the pricing plan).
monthly_fee Monthly price charged to the buyer (null or absent = the pricing plan).
label Label of the number (the number by default).

Response: phone_number: The number added (pool view).

DELETE /api/v1/domains/<domain_id>/number-pool/<phone_id>

Remove a number from the pool, as long as nobody holds it.

A number bought by an organization, held in its trash or in reserve (balance limbo) or whose order is pending cannot be removed (409 number_in_use). A number ordered from the provider is given back to it first (502/503 and nothing removed if the provider fails), unless release_at_provider is false.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
phone_id Phone number ID.

Query parameters

Name Description
release_at_provider false keeps the number at the provider and only removes it here (default true).

Response: message: Result message.

PUT /api/v1/domains/<domain_id>/number-pool/<phone_id>

Change the attributes and prices of a number of the pool (the fields given change).

Prices apply to the next purchase; a number already bought keeps what its owner paid.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
phone_id Phone number ID.

Request body

Name Description
country ISO 3166-1 alpha-2 country.
type local or tollfree.
area_code Area code.
city City.
region Province or state.
capabilities List of voice, sms.
setup_fee Installation price (null = the pricing plan).
monthly_fee Monthly price (null = the pricing plan).
label Label.

Response: phone_number: The number after the change (pool view).

POST /api/v1/domains/<domain_id>/number-pool/batch

Add many numbers to the pool in one request (JSON list or CSV).

Every row is validated on its own: the valid rows are added, the others are reported (a number already known is skipped, never an error). At most 5000 rows. Send JSON {numbers: [...]} where an item is an E.164 string or an object with the single-number fields, optionally {defaults: {...}} applied to every item; or {csv: "<text>"}; or the CSV itself (Content-Type text/csv, or a multipart file). CSV columns: number, country, type, area_code, city, region (or state, province), setup_fee, monthly_fee, capabilities (voice|sms), label; comma or semicolon separated, with a header.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Query parameters

Name Description
dry_run 1 validates and counts without writing (CSV bodies; JSON uses the dry_run field).

Request body

Name Description
numbers List of numbers (strings or objects).
defaults Fields applied to every item (country, type, city, region, capabilities, setup_fee, monthly_fee).
csv The CSV text, instead of numbers.
dry_run true validates without writing.

Response: created: Numbers added. skipped: List of {row, number, reason} (already_exists, duplicate_in_batch). errors: List of {row, number, field, message} for the invalid rows. total: Rows received. dry_run: Whether nothing was written. phone_numbers: The first 100 numbers added (pool view).

POST /api/v1/domains/<domain_id>/number-trash/purge

Give back to the pool, now, the numbers of the domain whose reservation has ended (the periodic task does the same).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: result: {examined, returned}.

Number provider webhook

Delivery of number orders by the provider (public, token in the URL).

POST /webhook/numbers/<token>

Delivery webhook of the number provider: an order was delivered or failed.

Public; the URL token is the domain's webhook_secret (settings.voice.did_provider). The body is read by the provider adapter ({order_id, status, number, message}). A delivered order puts the number in service; a failed one refunds the buyer. Unknown tokens, orders and replays answer 200 without doing anything.

Access: public (no authentication)
Rate limit: 120 per minute

Path parameters

Name Description
token The domain's webhook_secret.

Response: processed: Number of order updates applied.

SMS providers

Available providers, configuration schemas, per-team configuration.

GET /api/v1/providers

List available SMS providers.

Access: authenticated (JWT or API key)

Query parameters

Name Description
lang Language for descriptions (fr, en).

Response: List of available providers with basic info.

GET /api/v1/providers/<provider_name>/guide

Get configuration guide for a provider.

Access: permission configure_provider (now teams.provider)

Path parameters

Name Description
provider_name Provider type (one of GET /api/v1/providers).

Query parameters

Name Description
lang Language (fr, en).

Response: Step-by-step configuration guide.

GET /api/v1/providers/<provider_name>/schema

Get configuration schema for a provider.

Access: permission configure_provider (now teams.provider)

Path parameters

Name Description
provider_name Provider type (one of GET /api/v1/providers).

Response: Configuration schema with required fields.

DELETE /api/v1/providers/teams/<team_id>/provider

Remove provider configuration from a team.

Access: team access (team_id); permission configure_provider (now teams.provider)

Path parameters

Name Description
team_id Team ID.

Response: Success message.

GET /api/v1/providers/teams/<team_id>/provider

Get current provider configuration for a team.

Access: team access (team_id); permission configure_provider (now teams.provider)

Path parameters

Name Description
team_id Team ID.

Response: provider: The provider name (null when none). config: The provider configuration, secrets masked. configured: Whether a provider is set.

PUT /api/v1/providers/teams/<team_id>/provider

Save provider configuration for a team.

Access: team access (team_id); permission configure_provider (now teams.provider)

Path parameters

Name Description
team_id Team ID.

Request body

Name Description
provider Provider type (one of GET /api/v1/providers).
config Provider configuration.
test_first Whether to test connection before saving (default: true).

Response: team: The team, with its provider configuration. message: Confirmation.

Errors 400 validation_error: provider missing or unknown. 400 connection_error / test_failed: the test of the connection failed, nothing is saved (success false). Skip the test with test_first false.

POST /api/v1/providers/test

Test provider connection with given configuration.

Access: permission configure_provider (now teams.provider)

Request body

Name Description
provider Provider type (one of GET /api/v1/providers).
config Provider configuration.

Response: success: Whether the connection works. message: Confirmation, or the reason of the failure.

Errors 400 validation_error: provider missing. 400 configuration_error: unknown provider or invalid configuration (success false). 400 connection_error: the provider refused the connection, with its code (success false). 500 unknown_error: unexpected failure (success false).

Invitations

User invitations into an organization.

GET /api/v1/invitations

List invitations for current organization.

Access: permission members.admin in the organization (profiles)

Response: invitations: The invitations of the current organization. total: Number of invitations.

POST /api/v1/invitations

Create a new invitation.

Session (JWT) only: a request authenticated by an API key, ordinary or restricted, gets 403 (error forbidden, message "API key not allowed on this route"). An invitation is a way to sign in, and a key never hands one out. The body must be a JSON object (else 400 validation_error).

The answer is the same whether the address already has an account or not (it tells nothing about the address, and returns no profile): an invitation is created and e-mailed, valid 7 days. An address with no account follows the link to create its account (POST /auth/register). An address that already has an account is never attached to the organization by the invitation alone: the account accepts it itself, signed in, with POST /api/v1/invitations/accept (the link of its e-mail leads to the page that does it). Until then the invitation shows among the pending ones, can be resent and cancelled.

The e-mail goes through the SMTP server of the organization's domain and links to that domain's first production hostname, whatever X-Frontend-Host (the platform's server and FRONTEND_URL for an organization without domain).

An organization sends at most 50 invitations per hour (429 rate_limited, config RATELIMIT_INVITATIONS), counted for the organization, not for each sender.

An invitee who will use the messages (messages.read or messages.send among the permissions of the profiles) takes a place of the organization's messaging plan: beyond its max_users, 402 plan_limit with limit (users), plan, upgrade (the first plan with room, or null) and current (members who use the messages and pending invitations). The same check applies to every way such a member joins (acceptance of the invitation, POST /api/v1/users, a role or profiles changed), for every caller, platform and domain administrators included.

Access: permission members.admin in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Request body

Name Description
email Invitee email address (a valid address, at most 254 characters; else 400 validation_error with field email).
role Role for the new user (optional, default: agent; admin, manager or agent). Former way: prefer profile_ids.
profile_ids Profiles the member gets (optional; ids of GET /api/v1/organizations/current/profiles, they add up); when given, the role follows them.
team_id Team ID to assign (optional; a team of the organization).
organization_id Organization to invite into (optional; only a platform_admin, or a domain_admin of its domain, may name an organization other than their own; a string, else 400 validation_error).

Response: invitation: The invitation, with its token (shown here only). message: Confirmation. email_sent: Whether the e-mail went out (201). The answer is the same whether or not the address already belongs to an account (no profile is returned and nobody is added): an existing account accepts the invitation itself with POST /invitations/accept.

Errors 400 validation_error: email missing or invalid, role other than admin, manager or agent, profile_ids empty or naming a profile the organization cannot give (field profile_ids), team not in the organization, caller without organization, or an invitation of this organization to this address already pending. 402 plan_limit: the messaging plan has no room for one more user. 403 forbidden: organization_id of an organization the caller does not manage; or the role or the profiles give a permission the caller does not hold in the organization (field role or profile_ids; with members.manage alone, members.manage and members.admin are never given). 404 not_found: organization_id unknown. 409 conflict: the account is already a member of the organization.

DELETE /api/v1/invitations/<invitation_id>

Delete/cancel an invitation.

Access: permission members.admin in the organization (profiles)

Path parameters

Name Description
invitation_id Invitation ID.

Response: Success message.

POST /api/v1/invitations/<invitation_id>/resend

Resend an invitation email.

Session (JWT) only: a request authenticated by an API key, ordinary or restricted, gets 403 (error forbidden, message "API key not allowed on this route"), like the creation. The e-mail goes through the SMTP server of the organization's domain and links to that domain's first production hostname, whatever X-Frontend-Host (the platform's server and FRONTEND_URL for an organization without domain).

Access: permission members.admin in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
invitation_id Invitation ID.

Response: Success message.

Errors 404 not_found: invitation unknown. 403 forbidden: invitation of an organization the caller does not administer. 400 validation_error: already accepted. 500 configuration_error: no SMTP server for the domain. 500 email_error: the e-mail could not be sent.

POST /api/v1/invitations/accept

Accept, with an existing account, an invitation to an organization.

The account joins the organization with the profiles (else the role's profiles) and the team of the invitation: a membership is created, nothing else of the account changes (its current organization and team stay). Switch to the organization afterwards (POST /api/v1/organizations/switch). Only the owner of the invited address, signed in with that very account, can accept: the token alone, held by the inviter for one, is not enough. An invitation is used once: the link opened again by its owner, already a member of the organization, answers 200 with already_member true (and creates nothing).

Session (JWT) only: a request authenticated by an API key gets 403 (error forbidden, message "API key not allowed on this route").

The body must be a JSON object (else 400 validation_error).

Access: authenticated (JWT or API key)
API keys: refused, 403 forbidden (API key not allowed on this route)

Request body

Name Description
token Invitation token (from the link of the e-mail; a non-empty string, else 400 validation_error with field token).

Response: organization: The organization joined. role: The role the profiles amount to there (admin, manager or agent; a copy). already_member: true when the invitation had already been accepted by this account, which is a member of the organization. message: Confirmation. 404 not_found, whatever the cause (unknown, expired, used or cancelled invitation, or an invitation for another address). 402 plan_limit: the invitation gives the messages (messages.read or messages.send) and the organization's messaging plan has no room left for one more user (its place was kept while it was pending: this happens when the plan changed since); {limit (users), plan, upgrade, current}. The invitation stays usable.

GET /api/v1/invitations/verify/<token>

Verify an invitation token.

Public. Rate limited by the default limits, counted per token, whatever the X-API-Key or Authorization headers.

Access: public (no authentication)

Path parameters

Name Description
token Invitation token.

Response: valid: true. email: The invited address. role: The role of the invitation (domain_admin for an invitation to administer a domain). organization_name: Name of the organization (of the domain for a domain admin invitation).

Errors 404 not_found: unknown or expired token. 410 invitation_accepted: the invitation was already accepted, the account exists: sign in (email in the answer, the address the link was sent to).

Users

Organization user management.

GET /api/v1/users

List the users of the caller's organization.

A member who holds members.manage without members.admin (the former manager) only sees the users of their own team (none without a team); with members.admin, every user of their organizations (platform_admin: every account of the platform when no team is given; with the header X-Admin-Organization, the administered organization only). Super admins are never listed as members.

Access: permission manage_users (now members.manage)

Query parameters

Name Description
team_id Filter by team ID (must be a team the caller can access).
skip Pagination offset (default 0).
limit Pagination limit (default 50, max 100). A skip or limit that is not an integer is refused (400 validation_error).

Response: users: The users of the page. total: Number of users before skip and limit. skip: Offset applied. limit: Page size applied.

POST /api/v1/users

Create a user in the caller's organization.

A member with members.manage without members.admin (the former manager) only creates agents, placed in their own team; with members.admin, any organization role. The member gets the profiles of the role (admin: Admin; manager: User and the organization's Manager profile; agent: User); PUT /api/v1/organizations/current/members/<id>/profiles changes them. super_admin and domain_admin cannot be created here (platform permissions: PUT /api/v1/users/<id>/platform-permissions). 400 validation_error when the address already has an account (an invitation never reveals it: prefer POST /api/v1/invitations).

Session (JWT) only: a request authenticated by an API key, ordinary or restricted, gets 403 (error forbidden, message "API key not allowed on this route"). An account, even without a password, is a way to sign in (password reset by e-mail, OAuth), and a key never hands one out. The body must be a JSON object (else 400 validation_error).

Access: permission manage_users (now members.manage)
API keys: refused, 403 forbidden (API key not allowed on this route)

Request body

Name Description
email User email (a valid address, at most 254 characters; stored in lower case).
name User display name (1 to 100 characters, no control characters).
password User password (optional if sending invite). When given it follows the rule of PUT /auth/me/password (8 to 128 characters, at least 2 of lower case, upper case, digits and symbols, not a very common password, not the e-mail address); else 400 validation_error with field password.
role Organization role, translated into profiles (admin, manager or agent; default agent; one the caller may give, else 403 forbidden).
team_id Team ID (must belong to the organization).
language Preferred language (fr or en, default fr; else 400 validation_error with field language).

Response: Created user data.

Errors 402 plan_limit: the role gives the messages (messages.read or messages.send) and the organization's messaging plan has no room left for one more user (same check as POST /api/v1/invitations, for every caller): {limit (users), plan, upgrade (the first plan with room, or null), current (members who use the messages and pending invitations)}.

DELETE /api/v1/users/<user_id>

Delete a user of the caller's organization.

Access: permission manage_users (now members.manage)

Path parameters

Name Description
user_id User ID.

Response: Success message.

Errors 400 validation_error: the caller cannot delete themselves. 409 user_active: deactivate the user first. 403 forbidden: user out of reach. 403 member_of_other_organizations: the account also belongs to organizations the caller does not administer.

GET /api/v1/users/<user_id>

Get user details.

Users can view themselves; a member with members.manage without members.admin (the former manager) the agents of their own team; with members.admin, the users of a shared organization; platform_admin anyone, domain_admin the users of the organizations of their domains.

Access: authenticated (JWT or API key); Yourself; members.admin: the members of a shared organization; members.manage: the members of their own team there who manage nobody (neither members.manage nor members.admin); in both cases only a member whose permissions there the caller holds too (checked in the function: 403 forbidden otherwise).

Path parameters

Name Description
user_id User ID.

Response: User data.

PUT /api/v1/users/<user_id>

Update a user of the caller's organization.

The role and the team belong to ONE organization: the caller's (the user must be a member of it). The other fields belong to the whole account (every organization and every domain of the user): to change them on someone else the caller must administer every organization of the user (platform_admin, a domain_admin of all their domains, a member holding members.admin in all their organizations; with members.manage alone, an agent of their own team who belongs to no other organization), else 403 member_of_other_organizations. A value identical to the stored one is no change. The user changes their own profile freely, except the password (see below).

The body must be a JSON object (else 400 validation_error).

Access: permission manage_users (now members.manage)
API keys: refused when the body gives the role admin, domain_admin, super_admin; refused when the body contains password

Path parameters

Name Description
user_id User ID.

Request body

Name Description
name New name (optional; a string of 1 to 100 characters without control characters, else 400 validation_error with field name). Account-wide.
role New role in the caller's organization (optional; admin, manager or agent, never above the caller, never one's own): the member's profiles are replaced by those of the role (as POST /api/v1/users). An API key never gives the admin role: 403 (error forbidden, message "API key not allowed on this route").
is_active Activate or deactivate the user (optional; a caller who administers every organization of the user, never oneself). A deactivated user can no longer sign in and their sessions stop working. Account-wide.
team_id New team (optional; a team of the caller's organization, null takes the user out of any team there). Written on the membership of that organization only; the user must be a member of it.
language New language (optional; fr or en, else 400 validation_error with field language). Account-wide.
permissions Permissions granted to the account on top of its profiles (optional; the only grantable one is support). Only a platform_admin, or a domain_admin who administers every organization of the user, may change the grants, never their own; the permissions of the role in the list are ignored, so a form echoing the whole user is fine. Anyone else changing them gets 403 forbidden, a name that cannot be granted 400 validation_error with field permissions. permission_grants is an alias with the same rule (it is not a free field).
first_name New first name (optional, rewrites name). Account-wide.
last_name New last name (optional, rewrites name). Account-wide.
timezone IANA time zone (optional; null clears it). Account-wide.
password New password of ANOTHER account (optional): a password reset by an administrator, who must administer every organization of the user and chooses a password that follows the rule of PUT /auth/me/password (else 400 validation_error with field password). Your own password is not changed here (403 forbidden with field password): use PUT /auth/me/password, which asks for the current password. Session (JWT) only: with an API key, a body with a password gets 403 (error forbidden, message "API key not allowed on this route"), whoever the user, the key owner included.

Response: Updated user data.

Errors 402 plan_limit: the new role gives the messages (messages.read or messages.send) to a member who did not have them and the organization's messaging plan has no room left (same check as POST /api/v1/invitations, for every caller; nothing is changed): {limit (users), plan, upgrade, current}. 403 member_of_other_organizations: activating or deactivating the account, or setting the password of another user, reaches organizations the caller does not administer.

DELETE /api/v1/users/<user_id>/membership

Remove a member from the caller's organization, leaving the account alone.

The user leaves the caller's organization (the current one, or the one being administered with X-Admin-Organization) and nothing else: the account stays active in its other organizations, with its profile, password and sessions. Neither the activation of the account nor the administration of its other organizations is needed (unlike PUT /users/<id> with is_active and DELETE /users/<id>, which reach the whole account). The user's SIP endpoints of this organization are detached and disabled, and the default organization they had here is forgotten. When this was their current organization they move to another one of theirs.

A user whose only organization this is cannot be removed (409 only_organization): they are deactivated, then deleted. Never oneself (400 validation_error), never a member the caller may not act on there (403: members.admin, or members.manage for a member of their team who manages nobody, and every permission the member holds there held by the caller too), never a user who is not a member (404).

Access: permission members.admin in the organization (profiles)

Path parameters

Name Description
user_id User ID.

Response: Success message.

PUT /api/v1/users/<user_id>/role

Change the organization role of a user of the caller's organization.

Former way: the member's profiles are replaced by those of the role (admin: Admin; manager: User and the organization's Manager profile; agent: User); prefer PUT /api/v1/organizations/current/members/<id>/profiles. Never one's own role, never a role above the caller's (403 forbidden), never on a user who is not a member of the caller's organization (403).

The body must be a JSON object (else 400 validation_error).

Access: permission members.admin in the organization (profiles)
API keys: refused when the body gives the role admin, domain_admin, super_admin

Path parameters

Name Description
user_id User ID.

Request body

Name Description
role New role (admin, manager, agent; required, else 400 validation_error). An API key never gives the admin role: 403 (error forbidden, message "API key not allowed on this route").

Response: Updated user data.

Errors 402 plan_limit: the role gives the messages to a member who did not have them and the organization's messaging plan has no room left (as POST /api/v1/invitations): {limit (users), plan, upgrade, current}.

PUT /api/v1/users/<user_id>/team

Assign a member of the caller's organization to one of its teams.

The team belongs to ONE organization, the caller's (the current one, or the one being administered with X-Admin-Organization): the user must be a member of it (else 403) and the team one of its teams that the caller may use (else 400 validation_error). The team is written on the membership of that organization, whatever the current organization of the user: an administrator never changes the team the user has in another organization.

The body must be a JSON object (else 400 validation_error).

Access: permission members.admin in the organization (profiles)

Path parameters

Name Description
user_id User ID.

Request body

Name Description
team_id Team ID (null to remove from any team).

Response: user: The updated user. message: Confirmation.

API keys

Keys for programmatic access.

GET /api/v1/keys

List API keys of the caller, or of a user they administer.

Access: permission generate_api_keys (now api_keys.manage)

Query parameters

Name Description
user_id Owner of the keys (optional, defaults to the caller).

Response: api_keys: The keys (id, key_prefix, name, user_id, team_id, team_ids, integration_id, created_at, last_used_at, revoked), never the secret. total: Number of keys.

POST /api/v1/keys

Create a new API key for the caller.

JWT only: a request authenticated by an API key gets 403 (error forbidden, message "API key not allowed on this route"), so a key never mints another key. A key limited to a team acts as an agent of that team, whatever the role of its creator: it cannot administer anything nor read organization-wide data (billing, organization, members).

A key that is not limited to a team has the rights of its owner (an administrator): it is only minted with the CURRENT PASSWORD of the owner (current_password: 400 validation_error when missing, 401 invalid_password when wrong), so that a stolen access token (valid one hour) does not buy a key that outlives it. An account without a password (OAuth sign-in only) has none to give. Rate limited to 10 per hour per user (429 rate_limited): the password is guessed here otherwise. The key of a super_admin or a domain_admin goes no further than the administration of the owner's current organization.

A personal key is revoked when its owner changes the password or the e-mail address, resets the password, or is deactivated (the keys of an integration are not personal).

The body must be a JSON object: anything else (no body, a body that is not JSON, a list, a string...) gets 400 validation_error.

Access: permission generate_api_keys (now api_keys.manage)
API keys: refused, 403 forbidden (API key not allowed on this route)
Rate limit: 10 per hour

Request body

Name Description
name Key name/description (required, a non-empty string, else 400 validation_error).
team_id Restrict the key to this team (optional; the identifier, as a string, of a team the caller can access, else 400 validation_error).
current_password The password of the caller, required for a key that is not limited to a team (see above; ignored otherwise).
expires_in_days Lifetime of the key in days (optional, an integer from 1 to 365, else 400 validation_error with field expires_in_days; 90 when absent). Every key made here expires: it is refused (401) after expires_at, and a new one is made to replace it.

Response: api_key: The key record (same fields as in the list), without the secret. key: The secret of the key, in plain text. It is in this answer only (201): store it now. message: Confirmation. warning: Reminder that the key will not be shown again.

DELETE /api/v1/keys/<key_id>

Revoke an API key.

Access: permission generate_api_keys (now api_keys.manage)

Path parameters

Name Description
key_id API key ID.

Response: message: Confirmation that the key is revoked.

GET /api/v1/keys/<key_id>

Get API key details.

Access: permission generate_api_keys (now api_keys.manage)

Path parameters

Name Description
key_id API key ID.

Response: api_key: The key record (same fields as in the list), without the secret.

POST /api/v1/keys/<key_id>/revoke

Revoke an API key (alias of DELETE).

Access: permission generate_api_keys (now api_keys.manage)

Path parameters

Name Description
key_id API key ID.

Response: message: Confirmation that the key is revoked.

Website widget

Configuration of the form embedded on a website (permission integrations.manage).

GET /api/v1/widgets

List the website widgets of the current organization.

Access: permission integrations.manage in the organization (profiles)

Response: widgets: List of widgets with their embed code. total: Number of widgets.

POST /api/v1/widgets

Create a website widget for the current organization.

Access: permission integrations.manage in the organization (profiles)

Request body

Name Description
name Internal name of the widget.
team_id Team that receives the conversations.
phone_number Team number the welcome message is sent from.
welcome_message Text sent to the visitor's mobile, in the default language.
welcome_messages Welcome text per language, {fr, en} (optional; an empty language falls back to the default one).
language Default language, fr or en (optional, default fr); the widget follows the page language (<html lang> or data-lang) when it is fr or en.
texts Displayed texts per language, {fr: {title, description, button_label, success_message}, en: {...}} (optional; a flat object applies to the default language).
accent_color Hex color of the button (optional).
text_color Hex color of the button text (optional, default #ffffff).
allowed_origins Websites allowed to use the widget, hostnames (optional, empty = any).
enabled Whether the widget accepts submissions (optional, default true).

Response: widget: The created widget with its embed code.

DELETE /api/v1/widgets/<widget_id>

Delete a website widget.

Access: permission integrations.manage in the organization (profiles)

Path parameters

Name Description
widget_id Widget ID.

Response: Success message.

GET /api/v1/widgets/<widget_id>

Get a website widget.

Access: permission integrations.manage in the organization (profiles)

Path parameters

Name Description
widget_id Widget ID.

Response: widget: The widget with its embed code.

PUT /api/v1/widgets/<widget_id>

Update a website widget (same fields as creation, all optional).

Access: permission integrations.manage in the organization (profiles)

Path parameters

Name Description
widget_id Widget ID.

Response: widget: The updated widget. message: Confirmation.

POST /api/v1/widgets/<widget_id>/regenerate-key

Give the widget a new key: sites using the old embed code stop working.

Access: permission integrations.manage in the organization (profiles)

Path parameters

Name Description
widget_id Widget ID.

Response: widget: The widget with its new embed code (the old key stops working). message: Confirmation.

Integrations

Third-party applications' access to an organization's teams: API key limited to the exposed teams and signed outbound webhook (permission integrations.manage).

GET /api/v1/integrations

List the integrations of the current organization.

Access: permission integrations.manage in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Response: integrations: List of integrations (the API key itself is never returned again). total: Number of integrations. events_available: Every event a webhook can receive (what a new integration gets by default).

POST /api/v1/integrations

Create an integration for the current organization.

The API key is returned once, in api_key; it cannot be read back later (use regenerate-key to get a new one). The webhook secret is readable at any time.

The webhook URL must be https on port 443, at most 500 characters, without credentials (user:password@), and every address its host resolves to must be public (no loopback, private, link-local, reserved, multicast or carrier-grade NAT address, IPv4 or IPv6, nor an address of the platform itself). It is checked again before each delivery, which then fails with "address not allowed" if the name now points elsewhere. Redirects are not followed.

The body must be a JSON object: anything else is refused with 400 validation_error, message "The request body must be a JSON object".

Access: permission integrations.manage in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Request body

Name Description
name Name of the integration (required, a string of 80 characters max), shown as the sender of the messages it sends.
team_ids Teams of the organization exposed to the integration (optional, empty = every team of the organization; a team of another organization is refused with 400).
webhook_url Public https URL receiving the events (optional, 500 characters max; without it the integration only uses the API). Any other URL is refused with 400 validation_error, message "webhook_url must be a public https URL".
events Events to deliver (optional, default all): message.received, message.sent, message.status, conversation.updated, conversation.note. An unknown event is refused with 400.
enabled Whether the key and the webhook are active (optional, default true).

Response: integration: The created integration (201). api_key: The API key, shown only once.

DELETE /api/v1/integrations/<integration_id>

Delete an integration: its API key is revoked and its webhook stops.

Access: permission integrations.manage in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
integration_id Integration ID.

Response: Success message.

GET /api/v1/integrations/<integration_id>

Get an integration.

Access: permission integrations.manage in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
integration_id Integration ID.

Response: integration: The integration.

PUT /api/v1/integrations/<integration_id>

Update an integration: only the fields sent change.

A new webhook URL follows the rules of the creation (public https URL on port 443, 500 characters max); the URL already saved, sent again as is, is not checked again (so the integration can be renamed or disabled while its receiver is down). Every delivery checks the URL before the call.

The body must be a JSON object: anything else is refused with 400 validation_error, message "The request body must be a JSON object".

Access: permission integrations.manage in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
integration_id Integration ID.

Request body

Name Description
name New name (a string of 80 characters max), also shown as the sender of its messages (optional).
team_ids New list of exposed teams (optional; replaces the list, empty = every team of the organization); the API key follows at once.
webhook_url New public https URL (optional, 500 characters max); "" or null removes the webhook; the saved URL sent again unchanged is kept without a new check. A new URL resumes a paused webhook and resets its failure count and last error. Any other URL is refused with 400 validation_error, message "webhook_url must be a public https URL".
events New list of events (optional; replaces the list).
enabled false disables the API key and the webhook, true enables them again (optional).
webhook_paused false resumes a webhook paused after repeated failures (optional; true is ignored).

Response: integration: The updated integration.

GET /api/v1/integrations/<integration_id>/deliveries

Most recent webhook deliveries of an integration, newest first.

Deliveries are kept 7 days; the list holds the limit most recent ones, without pagination. The ping of /test is not listed.

Access: permission integrations.manage in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
integration_id Integration ID.

Query parameters

Name Description
limit Number of rows (optional, integer, default 50, at most 200; values out of 1..200 are clamped). Not an integer: 400 validation_error.

Response: deliveries: Event, URL, attempts, HTTP status, error ("HTTP <status>", "timeout", "connection error", "redirect not followed", "address not allowed" or "not sent"; never the content of the answer), timestamps and payload. total: Number of rows returned.

POST /api/v1/integrations/<integration_id>/regenerate-key

Replace the API key of an integration: the previous key stops working at once.

Access: permission integrations.manage in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
integration_id Integration ID.

Response: integration: The integration. api_key: The new API key in plain text, shown only once (the old key stops working). message: Confirmation.

POST /api/v1/integrations/<integration_id>/regenerate-secret

Replace the webhook signing secret of an integration.

Access: permission integrations.manage in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
integration_id Integration ID.

Response: integration: The integration with its new webhook secret. message: Confirmation.

POST /api/v1/integrations/<integration_id>/test

Send a ping event to the webhook URL and report the outcome (no retry).

The ping is signed like any event but has a reduced shape, {"id": "test", "event": "ping", "created_at", "organization_id", "integration": {"id", "name"}}, without team, conversation nor message, and its X-Textos-Delivery header is always test (do not deduplicate on it). It goes out even when the integration is disabled or its webhook paused, is not listed among the deliveries and does not count in the webhook health. The receiver should answer 2xx to ping and ignore the events it does not know.

The call follows the delivery rules: redirects are not followed, 5 s to connect, 10 s between two reads, 15 s in all; the body of the answer is neither kept nor returned. The ping waits 5 s at most for a free call slot (error "not sent" otherwise, nothing being sent). 10 tests per minute per organization (all its integrations together), then 429 rate_limited.

Access: permission integrations.manage in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)
Rate limit: 10 per minute

Path parameters

Name Description
integration_id Integration ID.

Response: success: Whether the endpoint answered 2xx in time. status_code: HTTP status returned by the endpoint, if any (null when no answer was received). error: null on success, else one of "HTTP <status>", "timeout", "connection error", "redirect not followed", "address not allowed" (the URL resolves to an address that is not public), "not sent" (no free call slot in time: Telma is busy, try again), "No webhook URL".

Website widget (public)

Endpoints called by visitors' browsers: script and conversation start. Unauthenticated, rate limited.

GET /widget/<key>.js

The embeddable script of a widget, with its display configuration inlined.

Never rate limited: every visitor of every customer site loads it, and in production they all arrive from the same address (a counter per address would be shared by all the sites). It is public, cached by browsers (5 minutes) and has no side effect.

Access: public (no authentication)
Rate limit: exempt from the general per-address limit

Path parameters

Name Description
key Widget key (from the embed code).

Response: JavaScript that renders the form on the host page.

POST /widget/<key>/start

Start a conversation from the widget: the team's welcome text is sent to the visitor.

Rate limited per widget (10 per minute, 60 per hour) and per destination number, whatever the widget (3 per hour, 10 per day), whatever other headers the request carries. Only the welcome texts handed to the SMS provider are counted: a slot is taken on each counter, atomically, right before the text is handed over, so simultaneous starts never send more than the limits; a refused request (unknown widget, other site, honeypot hit, invalid number, misconfigured widget, text already sent moments ago) is not counted, and the slots are given back when the text could not be handed to the provider (provider not enabled, insufficient balance). Beyond, 429 with error rate_limited and retry_after (also in Retry-After); a widget or a number already at its limit gets it before any other check.

Access: public (no authentication)
Rate limit: exempt from the general per-address limit

Path parameters

Name Description
key Widget key.

Request body

Name Description
phone The visitor's mobile number: a North American number (its 10 digits, with or without 1 or +1 in front, or "+" and the 10 digits), always counted and texted as +1 and the 10 digits; or an international number typed with "+" and another country code (8, 9 or 11 to 15 digits in all: "+" and 10 digits is read as a North American number). Anything else, a North American number whose area code or exchange starts with 0 or 1 included: 400 invalid_number.
lang Language of the visitor, fr or en (optional, default: the widget's default language); picks the welcome text.

Response: ok: true when the text was sent (or was sent moments ago to the same number). message: Text to show the visitor. already_sent: true when this number just received the text (a second start within a minute sends nothing).

Errors 404 not_found: widget unknown or disabled. 403 forbidden: the site is not among the allowed origins. 400 invalid_number. 429 rate_limited. 503 configuration_error: the widget has no team or number. 503 send_failed (or the code of the refusal, for example a provider not enabled or an insufficient balance): the text was not handed to the provider.

Push notifications

Firebase Cloud Messaging tokens of the Telma and VoXoS mobile apps (one entry per device).

POST /api/v1/push/register

Register the FCM token of this app installation for the current user.

Called at every login and whenever Firebase rotates the token. A token previously attached to another user is moved to the current one.

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
token FCM registration token.
platform "ios" or "android".
app "textos" (Telma app, Messages + Voice; default) or "ubinext" (Ubinext app): which notifications this installation receives (400 validation_error for any other value).
device_id Stable identifier of the installation (optional).
app_version App version (optional).
voip_token PushKit (VoIP) token of the installation, iOS only (optional): a hexadecimal string of 64 to 200 characters (stored lower-cased), null to erase it, absent to keep the stored one. The incoming-call push of an installation that has one goes through Apple's VoIP push (the app reports the call to CallKit at once) instead of FCM. A PushKit token belongs to one installation: it is taken off any other registration that carried it. 400 validation_error (field voip_token) for another value or on android.

Response: token: The registered token (masked), with has_voip_token.

GET /api/v1/push/tokens

Devices registered for push notifications by the current user.

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: tokens: List of the user's tokens (masked).

POST /api/v1/push/unregister

Stop notifying this app installation (logout on the device).

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
token FCM registration token.

Response: success: True. disabled: Whether a token of the current user was disabled.

Voice (VoXoS)

VoXoS endpoints (softphone provisioning), call flows, ring groups, sounds, blacklist, calls and recordings, call codes and call requests. Organizations with the voice service enabled only.

GET /api/v1/voice/access-numbers

Access numbers to dial to place a call with a call code (one per country, Canada first).

Legacy, without interface: the apps no longer use the call codes (an incoming call rings the softphone, outbound calls go through it); the route stays until it is removed.

Needs the call_codes capability (403 capability_required) on top of voxos; not open to API keys.

Access: permission voice.use in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Query parameters

Name Description
country ISO 3166-1 alpha-2 country (CA, US...). Default: the country of the user's phone, else every country.

Response: access_numbers: List of {country, number, label}.

GET /api/v1/voice/blacklist

Numbers, prefixes and regions the organization refuses (calls received and placed).

Access: permission voice.blacklist in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: entries: List of {id, description, type (number|prefix|region), number (number and prefix), region_id and area_codes (region), direction (both|inbound|outbound), note, created_at}.

POST /api/v1/voice/blacklist

Add an entry to the blacklist. An entry that already exists is updated and answered with 200.

The blacklist applies to every routing of the organization's numbers (flows, direct dial, groups, forward) for entries that apply to incoming calls, to the calls placed from the SIP endpoints and with call codes (the call is hung up and recorded with the disposition BLOCKED, nothing is charged). An entry written before E28 (no direction) keeps its old reading: incoming calls only in the flows that enable the blacklist.

Access: permission voice.blacklist in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
type number (default), prefix or region.
number Phone number (10 or 11 digits, with or without +) for a number; digits of the prefix for a prefix (with + they are international digits, without it a North American prefix: 514555 is 1514555).
region_id Region of GET /voice/blacklist/regions (region).
description Name of the entry (24 characters at most).
direction both (default), inbound or outbound.
note Why (optional, 120 characters at most).

Response: entry: The entry.

DELETE /api/v1/voice/blacklist/<entry_id>

Remove an entry from the blacklist.

Access: permission voice.blacklist in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
entry_id Entry ID.

Response: success: True.

PATCH /api/v1/voice/blacklist/<entry_id>

Rename an entry (its number, prefix or region never changes). The note and the direction can change too.

Access: permission voice.blacklist in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
entry_id Entry ID.

Request body

Name Description
description New name (24 characters at most).
note New note.
direction both, inbound or outbound.

Response: entry: The entry.

GET /api/v1/voice/blacklist/regions

Regions that can be blocked in one entry (provinces, toll-free and premium numbers), with their area codes.

Access: permission voice.blacklist in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: version: Version of the table (changes when a region changes). regions: List of {id, label (in the language of the request), labels {fr, en}, area_codes}.

GET /api/v1/voice/call-info

What the next call of one of the user's SIP endpoints will be like (the (i) of the app's dial pad).

Access: member of the organization; An enabled endpoint assigned to the user, in any of their organizations (404 not_found otherwise), judged in the organization of the endpoint: it needs the voice service and the sip capability there (403 voice_not_enabled, 403 capability_required with capability sip) and the user needs voice.use there (403 forbidden), whatever their current organization; not available to API keys (403 forbidden).
API keys: refused, 403 forbidden (API key not allowed on this route)

Query parameters

Name Description
endpoint_id An endpoint assigned to the user (required; see GET /voice/provision).

Response: call_info: {endpoint_id, caller_id {name, number} (what an outbound call presents: the label of the endpoint, the voice number its settings choose, else the number routed to it, else the first voice number of the organization), extension, recording (the next outbound call is recorded), recording_detail {endpoint (the endpoint's own setting), imposed_by (organization, number or null: a rule of the organization that wins on outbound calls), skip_allowed (the user may skip it for his next call), skip_next (the next outbound call is skipped)}, diagnostics {level (0 off, 1 summary, 2 detailed, 3 full), name, until}}.

PUT /api/v1/voice/call-info/recording

Record, or not, the calls one's own endpoint makes (its settings.recording, from the (i) of the app's dial pad). A rule of the organization (see recording_detail) still wins on outbound calls. Journalled.

Access: member of the organization; An enabled endpoint assigned to the user, in any of their organizations (404 not_found otherwise), judged in the organization of the endpoint: it needs the voice service and the sip capability there (403 voice_not_enabled, 403 capability_required with capability sip) and the user needs voice.use there (403 forbidden), whatever their current organization; not available to API keys (403 forbidden).
API keys: refused, 403 forbidden (API key not allowed on this route)

Query parameters

Name Description
endpoint_id An endpoint assigned to the user (required).

Request body

Name Description
enabled Record the calls of the endpoint (required).

Response: call_info: As GET /voice/call-info.

PUT /api/v1/voice/call-info/recording/skip-next

Skip, or not, the recording the organization imposes on the next outbound call of one's own endpoint (used once by the voice server, valid 15 minutes). Journalled.

Access: member of the organization; An enabled endpoint assigned to the user, in any of their organizations (404 not_found otherwise), judged in the organization of the endpoint: it needs the voice service and the sip capability there (403 voice_not_enabled, 403 capability_required with capability sip) and the user needs voice.use there (403 forbidden), whatever their current organization; not available to API keys (403 forbidden).
API keys: refused, 403 forbidden (API key not allowed on this route)

Query parameters

Name Description
endpoint_id An endpoint assigned to the user (required).

Request body

Name Description
skip True to skip the next outbound call, false to take the skip back (required).

Response: call_info: As GET /voice/call-info.

Errors 403 recording_skip_not_allowed: the organization does not let its users skip the recording. 409 recording_not_imposed: nothing imposes the recording of the next call (the endpoint's own setting decides).

POST /api/v1/voice/call-requests

Ask for a call code: dial the access number, then type the code, and the destination is called.

Legacy, without interface: the apps no longer use the call codes (an incoming call rings the softphone, outbound calls go through it); the route stays until it is removed.

The app shows access.tel_uri (tel:+15145550100,12345: access number, a pause, the code) to the phone dialler. The code lives 180 seconds; a new request cancels the user's pending one. The call is limited to max_duration seconds, computed from the balance and the tariff, and billed afterwards like any outbound call. Checks, in order: capability, body, verified phone (422 phone_not_verified), origin number (403 number_disabled), suspended account (402 account_suspended, E46: negative balance past the grace period), balance (402 insufficient_balance with balance), destination (422 blocked_call with reason invalid_destination, premium, blacklisted or no_rate), duration the balance allows (402), access number of the country (404 no_access_number), 60 requests per hour (429 rate_limited with retry_after).

Access: permission voice.use in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Request body

Name Description
from_number_id ID of the organization's number the call comes from (the caller ID shown to the callee).
to Destination number (E.164, or a North American number).
mode "access_code" (the only mode).
country Country of the access number (default: the country of the user's phone, else CA).

Response: call_request: {id, status "pending", from {number_id, number}, to, max_duration, expires_at, created_at}. access: {number, code, ttl, tel_uri}.

DELETE /api/v1/voice/call-requests/<request_id>

Cancel a call request (the user who made it only).

Legacy, without interface: the apps no longer use the call codes (an incoming call rings the softphone, outbound calls go through it); the route stays until it is removed.

A pending request becomes cancelled and its code stops working. A request that is already over (cancelled, expired, finished) answers success without any change; a call in progress (used or connected) answers 409 conflict.

Access: permission voice.use in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
request_id Call request ID.

Response: success: True.

GET /api/v1/voice/call-requests/<request_id>

Follow a call request (the user who made it only).

Legacy, without interface: the apps no longer use the call codes (an incoming call rings the softphone, outbound calls go through it); the route stays until it is removed.

Access: permission voice.use in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
request_id Call request ID.

Response: call_request: {id, status (pending, used, connected, expired, cancelled or failed), from {number_id, number}, to, max_duration, expires_at, created_at, used_at, connected_at, ended_at, answered_seconds, failure, call_id}; the last fields appear when they exist. A pending request past its expiry reads expired.

GET /api/v1/voice/calls

Call records (CDR) of the organization, newest first by default.

Needs the voxos capability (403 voice_not_enabled otherwise). A member without voice.calls.all sees only the calls of their own SIP endpoints (made or rung); with it, every call of the organization.

Access: member of the organization; voice.use in the current organization; with endpoint_id, judged in the organization of the endpoint instead (whatever the current organization), which needs the voice service and the sip capability (403 voice_not_enabled, 403 capability_required with capability sip, 403 forbidden without voice.use there).
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Query parameters

Name Description
skip Pagination offset (default 0; below 0 counts as 0).
limit Page size (default 50, brought back into 1-100). A skip or limit that is not an integer is refused (400 validation_error with field).
direction "inbound" or "outbound" (aliases "in" and "out"; optional).
from Calls started at or after this ISO 8601 date or datetime (optional; no time zone means UTC).
to Calls started at or before this ISO 8601 datetime; a date alone (2026-10-31) covers that whole day (optional).
number_id ID of the phone number the calls went through (optional).
status "all" (default), "answered", "missed" (not answered, not blocked) or "blocked" (hung up by the blacklist).
call_type direct, flow, forward, voicemail, screened, blocked or callcode (optional).
sort start (default), duration, cost, caller or callee.
order "desc" (default) or "asc".
endpoint_id Only the calls of this SIP endpoint, which must be assigned to the user (404 not_found otherwise, whatever the user's permissions): those it placed or that rang it, in the organization of the endpoint (any of the user's organizations). The history of the app, the same on every device of the endpoint. Each call then carries mine.

Response: calls: The page of calls (see GET /voice/calls/<id> for the fields). With endpoint_id, each one has mine {direction (outbound: the endpoint placed it, internal calls included; inbound: it rang), number (the other party: E.164 or short number), name (caller id name of an incoming call, label of an endpoint for a short number, else null), answered (by this endpoint, on any of its devices)}. total: Number of calls matching the filters, whatever the page. skip: Offset applied. limit: Page size applied.

GET /api/v1/voice/calls/<call_id>

One call record of the organization (without voice.calls.all: a call of the user's own SIP endpoints, 404 otherwise).

Access: permission voice.use in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
call_id Call ID.

Response: call: id, organization_id, phone_number_id, endpoint_id, caller and callee (the dialplan digits), from ({number, e164}) and to ({number, e164}, plus extension and label for a short number: the endpoint called or a test number), direction (an internal call is outbound), call_type (direct, flow, forward, voicemail, screened, blocked, callcode, internal), status (answered, voicemail, blocked, busy, failed: it never rang, no_answer: an outbound call nobody took, missed: an incoming call nobody took), dial_status, call_request_id, forwarded, clid, start, answer, end, duration, billsec, disposition, cost and currency (null until rated), billing (rating state, null until looked at), source_number and access_number (call-code calls), voicemail (the call ended in the voicemail), block_reason (paused, sleep, channel_limit, blacklist or account_suspended for a blocked call), recording ({state (processing, available, failed, deleted, expired), available_at, expires_at, size, duration} or null), inserted_at.

GET /api/v1/voice/diagnostics

Diagnostics reports of the organization's calls, newest first.

Needs the sip capability on the organization.

Access: permission voice.endpoints in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Query parameters

Name Description
endpoint_id Only the reports of this endpoint (optional).
skip Pagination offset (default 0).
limit Page size (default 50, at most 100).

Response: diagnostics: The page: id, endpoint_id, endpoint_name, level, direction, remote, sip_call_id, started_at, ended_at, created_at, size, app, problems (what the app found wrong, e.g. microphone_silent). total: Number of reports matching the filter. skip: Offset applied. limit: Page size applied.

POST /api/v1/voice/diagnostics

Upload the diagnostics report of a call, made by the app at its end when the diagnostics of the endpoint are on.

Only the parts of the current level of the endpoint are kept (a part of a higher level is dropped); SIP credentials (Authorization headers) are removed. Kept 30 days.

Access: member of the organization; An enabled endpoint assigned to the user, in any of their organizations (404 not_found otherwise), judged in the organization of the endpoint: it needs the voice service and the sip capability there (403 voice_not_enabled, 403 capability_required with capability sip) and the user needs voice.use there (403 forbidden), whatever their current organization; not available to API keys (403 forbidden).
API keys: refused, 403 forbidden (API key not allowed on this route)
Rate limit: 120 per hour

Request body

Name Description
endpoint_id The endpoint of the call, assigned to the user (required).
sip_call_id Call-ID of the call.
direction inbound or outbound.
remote The other party (number or name).
started_at ISO 8601 dates of the call.
ended_at ISO 8601 dates of the call.
app {version, build, platform, os, device} of the app.
summary Level 1 and above: one object (network, transport, srtp, codec, sip steps, rtp counters, microphone, playback, errors, problems).
timeline Level 2 and above: one sample per second (at most 7200).
sip_messages Level 2 and above: the SIP messages of the call as text (at most 500).
logs Level 2 and above: the app's log lines of the call (at most 5000).
rtp_trace Level 3: the RTP packet headers sent and received (at most 60000), never the audio.

Response: diagnostic: {id, level} of the stored report.

Errors 400 validation_error: diagnostics off for the endpoint (field endpoint_id), report too large for its level (field size: 64 KB at level 1, 1 MB at level 2, 4 MB at level 3) or a malformed part.

DELETE /api/v1/voice/diagnostics/<diagnostic_id>

Delete a diagnostics report.

Access: permission voice.endpoints in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
diagnostic_id Report ID.

Response: success: True.

GET /api/v1/voice/diagnostics/<diagnostic_id>

One diagnostics report, whole, with what the voice server saw of the call.

Access: permission voice.endpoints in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
diagnostic_id Report ID.

Response: diagnostic: The fields of the list, plus summary, timeline, sip_messages, logs, rtp_trace (those of its level) and server {call (the call record matched by endpoint and time, as GET /voice/calls/<id>, or null), qos (the RTP statistics the voice server computed for each channel of the call at hang up: channel, rx/tx packets, lost, jitter, rtt)}.

GET /api/v1/voice/directory

The other endpoints of the organization of one's own endpoint, to call them by their short number (the contacts of the app). Never an SIP identifier nor a password.

Access: member of the organization; An enabled endpoint assigned to the user, in any of their organizations (404 not_found otherwise), judged in the organization of the endpoint: it needs the voice service and the sip capability there (403 voice_not_enabled, 403 capability_required with capability sip) and the user needs voice.use there (403 forbidden), whatever their current organization; not available to API keys (403 forbidden).
API keys: refused, 403 forbidden (API key not allowed on this route)

Query parameters

Name Description
endpoint_id An endpoint assigned to the user (required); the organization is its own.

Response: entries: [{id, label (the label of the endpoint, else its user's name, else its short number), extension, user_name}] of the enabled endpoints of the organization that have a short number, sorted by label, without the endpoint asking.

GET /api/v1/voice/endpoints

SIP endpoints (softphones, desk phones) of the current organization.

Needs the sip capability on the organization: 403 capability_required (with capability: sip) without it, 403 voice_not_enabled without voxos.

Access: permission voice.endpoints in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: endpoints: [{id, name, label, organization_id, user_id, user {id, name, email}, server_id, server {id, name}, sip_server, sip_port, username, password, description, extension, enabled, max_contacts (null: the platform's), locked ({until, at, failures} while the voice server keeps it locked after wrong passwords, else null), settings (see PUT /voice/endpoints/<id>), diagnostics, created_at, updated_at}].

POST /api/v1/voice/endpoints

Create a SIP endpoint; its username and password are generated.

Needs the sip capability on the organization: 403 capability_required (with capability: sip) without it, 403 voice_not_enabled without voxos.

The SIP username (also its name) is derived from the account code of the organization (3 letters and 4 digits, given with its first endpoint, e.g. cli0001), a 0, then the short number: cli0001042 for 42; a letter is added when that name is taken (cli0001042a). 409 conflict when no account code is left for the name of the organization.

Access: permission voice.endpoints in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
label Display name of the endpoint (e.g. "Poste de Marie"), the caller name of its calls (required): 200 characters at most, no line break or control character (400 validation_error, field label).
user_id User of the organization (a member: 400 validation_error otherwise) who uses it from the mobile app (optional).
server_id Voice server to place it on (optional, default: the least loaded enabled server).
extension Short number (required, 400 validation_error with field extension without it): 2 to 5 digits, not starting with 0, unique in the organization (409 conflict otherwise); another endpoint dials it to call this one. Reserved: 600, 601 (tests), 112, 999 and the N11 numbers such as 911 (400 validation_error, field extension). It never changes afterwards.
settings Settings of the endpoint (optional), as in PUT /voice/endpoints/<id>.
max_contacts Devices registered at once on the endpoint, 1 to 10 (optional; null or absent: the platform's, 3 by default). The app on a phone or a tablet and a SIP desk phone may share the endpoint: they all ring together and the first to answer takes the call; beyond the limit, the oldest registration is replaced (400 validation_error, field max_contacts).

Response: endpoint: The created endpoint with its credentials.

DELETE /api/v1/voice/endpoints/<endpoint_id>

Delete an endpoint; numbers routed to it lose their routing.

Needs the sip capability on the organization: 403 capability_required (with capability: sip) without it, 403 voice_not_enabled without voxos.

Access: permission voice.endpoints in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
endpoint_id Endpoint ID.

Response: success: True.

PUT /api/v1/voice/endpoints/<endpoint_id>

Rename an endpoint, assign it to a user, enable or disable it, change its settings.

Needs the sip capability on the organization: 403 capability_required (with capability: sip) without it, 403 voice_not_enabled without voxos.

Access: permission voice.endpoints in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
endpoint_id Endpoint ID.

Request body

Name Description
label Display name (optional), same rules as at the creation.
user_id User of the organization, or null to unassign (optional).
enabled Whether the endpoint can register (optional).
server_id Move the endpoint to another voice server (optional); the app picks the new address at its next start.
extension Short number (as at the creation), only for an endpoint that has none yet (created before short numbers were required); the short number of an endpoint never changes and is never removed (400 validation_error, field extension).
max_contacts Devices registered at once on the endpoint, 1 to 10, or null for the platform's (optional), as at the creation.
diagnostics Diagnostics of the calls of the endpoint, {level (0 off, 1 summary, 2 detailed, 3 full), hours (1 to 168, default 24: they stop by themselves)}; the app uploads a report at the end of each call (GET /voice/diagnostics).
settings Settings of the endpoint (the fields given change): language (fr, en, es or null: the language of the messages it hears on its own calls), ring_seconds (5 to 120, or null: how long it rings before its no-answer handling; null = what the routing or its box says, 30 s for an internal call), dnd (do not disturb: it never rings, the call goes to its busy handling; turning it on or off here makes it without end), forward {always, busy, no_answer} (a 10-digit North American number or the short number of another endpoint of the organization; null = none; always never rings it), follow_me {enabled, numbers (1 to 3 external numbers rung together once it did not answer), ring_seconds (10 to 120)}, recording (the calls it makes are recorded), caller_id_number_id (a voice number of the organization its outbound calls present; null = the number routed to it, else the first voice number), allowed_networks (up to 20 IPv4 or IPv6 addresses or CIDR networks: its outbound calls go out only from a device in one of them; empty: anywhere; registration and incoming calls are not restricted), virtual (an extension without a phone that forwards its calls, counted as a virtual user of the phone system). The answers also carry dnd_until (ISO 8601 in UTC, or null: no end), the end of a do not disturb set by the user for their whole account (PUT /auth/me/dnd), never accepted here (400); the dnd answered is the one in force (false once dnd_until passed). 400 validation_error with field; 422 destination_not_allowed (with field, reason, country) for a forward the organization may not make.

Response: endpoint: The updated endpoint (with settings).

POST /api/v1/voice/endpoints/<endpoint_id>/reset-password

Generate a new SIP password for an endpoint (phones must be reconfigured).

Needs the sip capability on the organization: 403 capability_required (with capability: sip) without it, 403 voice_not_enabled without voxos.

Access: permission voice.endpoints in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
endpoint_id Endpoint ID.

Response: endpoint: The endpoint with its new password.

POST /api/v1/voice/endpoints/<endpoint_id>/unlock

Unlock an endpoint the voice server locked after repeated wrong passwords (its outbound calls were refused). Journalled. Change its password first if it may have leaked.

Needs the sip capability on the organization.

Access: permission voice.endpoints in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
endpoint_id Endpoint ID.

Response: endpoint: The endpoint (as GET /voice/endpoints), locked null.

GET /api/v1/voice/endpoints/status

Registration state of the organization's endpoints, as seen by the voice server.

Needs the sip capability on the organization: 403 capability_required (with capability: sip) without it, 403 voice_not_enabled without voxos.

The app of the mobile app is not registered between two calls: the voice server wakes it by a push when a call comes in. reachable names the endpoints it would wake (their user has a device that receives the incoming-call push), whatever their registration state. dnd names the endpoints in do not disturb now (set by their user in the mobile app or in the endpoint's settings): they never ring, whatever their registration state.

Access: permission voice.endpoints in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: statuses: {endpoint name: state} ("Not in use", "In use", "Unavailable"...), empty when the voice server is unreachable. reachable: {endpoint name: {devices, platforms, last_seen, app_versions}}: the endpoints whose user has a device woken by a push for an incoming call (devices: how many; platforms: ios, android; last_seen: ISO 8601, the last time the app registered that device; app_versions: versions of the app); the others are absent. dnd: {endpoint name: end}: the endpoints in do not disturb now, with its end (ISO 8601 UTC, null without end); the others are absent.

GET /api/v1/voice/flows

Voice flows of the organization (what happens to a call: prompts, menus, ringing, voicemail).

Reading with voice.use; creating, changing and deleting need voice.routing.

Access: permission voice.use in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: flows: List of flows, by name, each with active_on ([{phone_number_id, number}]: the numbers that run it).

POST /api/v1/voice/flows

Create a voice flow. The name is unique in the organization: a name already taken gets " (1)", " (2)"...

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
name Flow name (80 characters at most).
language "fr" or "en" (prompts and voicemail language).
languages {fallback, switch_key, extras [{lang, fallback, switch_key, sounds {sound id: translated sound id}}]}: extra languages of the menu, the key that switches to each one and the sounds played in it (a sound without translation is played as is).
cid_prefix Text prepended to the caller name on the phones (optional); 20 characters at most (a longer text is cut), a string without control characters or line breaks and without double quotes, backslashes, commas, ampersands, pipes, dollar signs, braces and backticks (400 validation_error, field cid_prefix).
business_hours {enabled, timezone, schedule {mon..sun: [["09:00","17:00"]]}, closures [{start, end, repeat none|yearly|weekly, until, ranges, label}], exceptions [{date, ranges}], closed_step}. A closure is dated (the year counts): repeat "none" is one occurrence, "yearly" comes back every year, "weekly" every 7 days, until ends the repetition and ranges are the hours the line stays open on those days.
closed Menu used outside the business hours, {steps, keys, retry}, same form as the open one (optional, the business_hours.closed_step is used when it is empty).
blacklist_enabled Hang up on the callers of the organization's blacklist (default true).
steps Ordered steps: {action: play|menu|wait|dial|groupdial|forward|goto|voicemail|hangup|back, sound_id (message played before the action), seconds, endpoint_id, group_id, flow_id, mailbox (voicemail step: the id of a voice number of the organization whose voicemail takes the message, or of a voicemail box of GET /voice/mailboxes with the voicemail_boxes capability, or empty for the number called), number, ext, screening 0|1|2, display caller|called, on_fail (a step), timeout, repeat (menu: times the prompt plays, 0 means 2, 5 at most), digit_timeout, extension_dial (menu: the caller may type the short number of an endpoint of the organization; a key of the menu wins, a prefix conflict is settled by digit_timeout; with it, on_fail is allowed on the menu: what follows when the endpoint dialled does not answer, else back to the menu), push_enabled, system_sound (play and menu, instead of sound_id: 1 to 6 standard prompts of GET /voice/system-prompts, only those names (another one: 400 validation_error with the field of the step), fr: or en: before a name fixes its language, an item {name, only: [fr|en]} is played only when the call is in one of those languages), wait ring|music (dial and groupdial: what the caller hears while ringing, absent = the setting of the number)}.
keys {key: step | {description, steps [step], then menu|hangup}}: what a key does during a menu step; a key has 1 to 4 characters among 0-9, * and #.
retry {attempts, step}: how many times the steps repeat, then what to do.

Response: flow: The created flow.

DELETE /api/v1/voice/flows/<flow_id>

Delete a flow. Refused (409 flow_in_use with active_on) while numbers run it, unless ?force=true, which also takes the routing off those numbers. Other flows or groups that jump to it always refuse (409 conflict).

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
flow_id Flow ID.

Query parameters

Name Description
force true to delete a flow that numbers run (their calls are no longer routed).

Response: success: True.

GET /api/v1/voice/flows/<flow_id>

One voice flow.

Access: permission voice.use in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
flow_id Flow ID.

Response: flow: The flow with active_on.

PUT /api/v1/voice/flows/<flow_id>

Update a voice flow (same fields as the creation, all optional; a key not given keeps its value).

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
flow_id Flow ID.

Response: flow: The updated flow.

POST /api/v1/voice/flows/<flow_id>/assign

Set the numbers that run a flow: the list is the full set, a number left out loses its routing to this flow, a number listed is routed to it (replacing its former routing).

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
flow_id Flow ID.

Request body

Name Description
phone_number_ids Voice numbers of the organization that run the flow (empty list: none).

Response: flow: The flow with its new active_on.

POST /api/v1/voice/flows/<flow_id>/duplicate

Copy a flow under a new name (made unique like on creation); the copy is not assigned to any number.

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
flow_id Flow ID.

Request body

Name Description
name Name of the copy.

Response: flow: The copy.

GET /api/v1/voice/groups

Call groups of the organization.

Reading with voice.use; creating, changing and deleting need voice.routing.

Access: permission voice.use in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: groups: List of groups, each with stages (lists of members), members, fallback and used_by ({flows, numbers}).

POST /api/v1/voice/groups

Create a call group: stages rung one after the other, the members of a stage ring together.

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
name Group name.
stages Stages, each a list of members {type "endpoint", endpoint_id} or {type "external", number (North American), ext (digits sent once the call is answered)}. A stage with an extension only holds external numbers sharing it. At most 10 members per stage and 20 in all.
endpoint_ids Endpoints of the group (former shape, used when stages is absent).
strategy "ringall" (all at once, default) or "sequential" (one after the other); former shape, with endpoint_ids.
timeout Ring time of each stage in seconds, 5 to 120 (default 20).
push_enabled Wake the mobile apps by push (default true).
ringing "generated" (the caller hears the ringing tone of the platform, default) or "real" (the caller hears the ringing of the called phones).
fallback Step run when nobody answers: play, forward, voicemail, hangup, dial or goto (optional).

Response: group: The created group.

DELETE /api/v1/voice/groups/<group_id>

Delete a call group; refused (409 conflict with used_by) while a flow or a number routing uses it.

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
group_id Group ID.

Response: success: True.

PUT /api/v1/voice/groups/<group_id>

Update a call group (same fields as the creation, all optional).

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
group_id Group ID.

Response: group: The updated group.

GET /api/v1/voice/moh

Music on hold of the organization: the free library of the platform, its own tracks, the playlists it may choose, and what it plays. Held calls always hear the chosen playlist; ringing plays it instead of the ringback tone while phones ring (groups, flows, forwards).

Access: permission voice.use in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: library: Tracks of the free library [{id, name, duration_s, collection, author, author_url, license {name, url}, source_url}]. tracks: Tracks of the organization [{id, name, duration_s, size, rights_confirmed, used_by [{type class, id, name}]}]. classes: Playlists it may choose [{id, name, name_en (English name of a playlist of the platform, null for its own), platform, platform_default, sound_ids, order random|alpha}]: those of the platform, then its own. settings: Choice of the organization {class_id (null = the default of the platform), ringing}. numbers: Its voice numbers [{id, number, label, class_id (null = the organization's), ringing (null = the organization's)}]. limits: Limits of a track {max_per_account, min_duration_s, max_duration_s, max_bytes, formats}.

POST /api/v1/voice/moh/classes

Create a playlist of the organization (20 at most) from tracks of the library and of its own.

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
name 1 to 60 characters (required).
sound_ids Tracks, 1 to 50, in this order (required).
order random (default) or alpha (the order of the names).

Response: class: The playlist.

DELETE /api/v1/voice/moh/classes/<class_id>

Delete a playlist of the organization; the organization and the numbers that chose it play the default music.

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
class_id Playlist ID.

Response: success: True.

PUT /api/v1/voice/moh/classes/<class_id>

Change a playlist of the organization (those of the platform cannot change: 404).

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
class_id Playlist ID.

Request body

Name Description
name 1 to 60 characters (optional).
sound_ids Tracks, 1 to 50 (optional).
order random or alpha (optional).

Response: class: The playlist.

PUT /api/v1/voice/moh/numbers/<phone_id>

What one voice number plays, over the choice of the organization.

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
phone_id Phone number ID.

Request body

Name Description
class_id Playlist ID, or null = the organization's (optional).
ringing true, false, or null = the organization's (optional).

Response: number: {id, number, label, class_id, ringing}.

PUT /api/v1/voice/moh/settings

What the organization plays: a playlist (its own or one of the platform; null = the default of the platform) and whether the music replaces the ringback tone while phones ring.

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
class_id Playlist ID, or null (optional).
ringing true: the music instead of the ringback tone; false: the ringback tone (optional).

Response: settings: {class_id, ringing}.

POST /api/v1/voice/moh/tracks

Upload a track of music on hold (multipart/form-data). The organization must confirm it holds the rights to broadcast it on the phone (who and when are kept). Limits: 30 tracks, wav/mp3/m4a/ogg, 20 MB, 5 seconds to 15 minutes; refused uploads answer 400 with a code (format_invalid, empty_file, file_too_large, audio_unreadable, duration_too_short, duration_too_long, account_limit).

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
file The audio file (required).
name Name of the track (default: the file name).
rights_confirmed true, the confirmation of the broadcast rights (required).

Response: track: The created track.

DELETE /api/v1/voice/moh/tracks/<sound_id>

Delete a track of the organization; its playlists lose it (a playlist left empty plays the default music).

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
sound_id Track ID.

Response: success: True.

PUT /api/v1/voice/moh/tracks/<sound_id>

Rename a track of the organization.

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
sound_id Track ID.

Request body

Name Description
name New name (required).

Response: track: The track.

GET /api/v1/voice/moh/tracks/<sound_id>/file

The audio of a track, to listen to it in the portal: one of the library or one of the organization.

Access: permission voice.use in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
sound_id Track ID.

Response: The file (audio/*).

GET /api/v1/voice/my-endpoints

The endpoints of the current user in the current organization, with their settings (to set one's own do not disturb, forwards and follow me).

Needs the sip capability on the organization. Not available to API keys.

Access: permission voice.use in the organization (profiles); Only the endpoints assigned to the user.
API keys: refused, 403 forbidden (API key not allowed on this route)

Response: endpoints: [{id, label, extension, enabled, settings (as GET /voice/endpoints)}].

PUT /api/v1/voice/my-endpoints/<endpoint_id>/settings

Set one's own endpoint: do not disturb, forwards, follow me and the recording of the calls it makes (the other settings are an administrator's). A change of the recording is journalled, as PUT /voice/call-info/recording; a rule of the organization still wins on outbound calls.

Needs the sip capability on the organization. Not available to API keys.

Access: permission voice.use in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
endpoint_id An endpoint of the current user in the current organization (404 not_found otherwise).

Request body

Name Description
dnd As the settings of PUT /voice/endpoints/<id>. Another setting: 400 validation_error; a forward the organization may not make: 422 destination_not_allowed.
forward As the settings of PUT /voice/endpoints/<id>. Another setting: 400 validation_error; a forward the organization may not make: 422 destination_not_allowed.
follow_me As the settings of PUT /voice/endpoints/<id>. Another setting: 400 validation_error; a forward the organization may not make: 422 destination_not_allowed.
recording As the settings of PUT /voice/endpoints/<id>. Another setting: 400 validation_error; a forward the organization may not make: 422 destination_not_allowed.

Response: settings: The settings of the endpoint after the change.

GET /api/v1/voice/numbers

Phone numbers of the organization with voice enabled, and how each one is routed.

A number gets its voice from the platform (a trunk set by the platform administrator for a number of a platform account) or from the domain administrator (a number of the domain's own accounts, on its own trunks); the organization decides where its calls go (permission voice.routing, PUT /voice/numbers/<id>/routing).

Access: permission voice.use in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Query parameters

Name Description
status active, paused, released (the trash) or pending; default: everything but the trash.
q Text matched in the name and in the digits of the number.
skip Pagination offset (default 0).
limit Page size (default 50, at most 100).
with_stats 1 adds stats ({calls, missed, duration, cost}, see GET /voice/stats) to each number, over the calls of the period (optional; 0 or absent: no figures); without voice.calls.all, only the calls of the user's own endpoints.
from With with_stats, only the calls started at or after this ISO 8601 date or datetime (optional).
to With with_stats, only the calls started at or before this ISO 8601 datetime; a date alone covers the whole day (optional).
direction With with_stats, only the calls of this direction, inbound or outbound (optional).

Response: numbers: The page of phone numbers, each with state {status, enabled, sleep, released_at, expires_at}, features {voicemail, recording, sms}, routing (null when calls are refused), server_id and, with with_stats, stats. total: Number of numbers matching the filters. counts: {active, paused, released, pending} after the text filter. skip: Offset applied. limit: Page size applied.

PUT /api/v1/voice/numbers/<phone_id>/routing

Route the incoming calls of a number.

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
phone_id Phone number ID (voice-enabled, in the organization).

Request body

Name Description
action "dial" (ring an endpoint), "dial-then-playback" (ring, then play a message), "playback", "voicemail", "flow" (run a voice flow), "groupdial" (ring a group), "forward" (forward to an external number), "custom" (sets the AGI variables of variables, then runs next_action; reserved to the platform and domain administrators, 403 forbidden for a member of the organization), or null to refuse calls.
endpoint_id Endpoint to ring (dial actions).
flow_id Voice flow to run (flow).
group_id Ring group (groupdial).
forward_number External number to forward the calls to (forward), 10-digit North American; billed as an outbound call. 422 destination_not_allowed (with reason and country) when the organization's forward countries or the premium-rate block refuse it (E47).
timeout Ring time in seconds (default 30).
push_enabled Wake the mobile app by push before ringing (default true).
sound_id Sound of the organization to play (playback, dial-then-playback): uploaded or recorded with POST /voice/sounds; it wins over sound. 400 validation_error (field sound_id) for a sound of another organization.
sound Name of a sound file of the voice server to play instead (playback, dial-then-playback), such as "hello-world" or "custom/welcome": 1 to 64 letters, digits, "_", ".", "/" or "-". 400 validation_error (field sound) for anything else, a line break included.
mailbox Voicemail box (voicemail): the id of a voice number of the organization or of a voicemail box (GET /voice/mailboxes); empty: the box of the number. 400 validation_error (field mailbox) for anything else.
variables Object of AGI variables, names [A-Za-z_][A-Za-z0-9_]* and plain string or number values (custom).
next_action AGI action run after the variables, default hangup (custom).

Response: number: The phone number with its new routing.

GET /api/v1/voice/provision

SIP account of the current user for the Voice tab of the Telma mobile app.

Called by the app after login and when the organization changes. The voice accounts of a user are the enabled SIP endpoints assigned to them in every organization that has the voice service and the sip capability, whatever the current organization and whether or not they are a member of it (the assignment by a member who has voice.endpoints there is the grant; no permission of the current organization is needed): the Voice tab shows as soon as there is one. The provisioned one is the one asked with endpoint_id, otherwise the first of the current organization, otherwise the first one (sorted by organization then name); endpoints lists them all so that the app can offer the choice. The app registers its push token with POST /api/v1/push/register (app: "textos") so that incoming calls wake it up.

Without any voice account, the error tells why for the current organization: 403 voice_not_enabled without voxos, 403 capability_required (with capability: sip) without sip, 404 no_endpoint otherwise.

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Query parameters

Name Description
endpoint_id Endpoint to provision among the user's (optional); an unknown or disabled one falls back to the default.

Response: sip: {server, port, transport, srtp, username, password, display_name} of the user's SIP account; the server is the one the endpoint is placed on, transport (udp, tcp or tls) one of its active listeners; srtp is "sdes" when the transport is tls (the app offers SRTP with keys in the SDP, AES_CM_128_HMAC_SHA1_80, and applies it when the answer accepts it), null otherwise. endpoint: {id, name, username, extension, label (the label of the endpoint, else the user's name, else its name: what the app shows instead of the SIP account), organization {id, name}} of the provisioned voice endpoint. endpoints (array): Every voice account of the user, {id, name, username, extension, label, organization {id, name}}, sorted by organization then name. call_info: What the next call of the provisioned endpoint will be like, as GET /voice/call-info (caller ID, recording, diagnostics level). organization: {id, name} of the organization of the provisioned endpoint. push: {app} the value to register the push token with.

Errors 404 no_endpoint: no enabled SIP endpoint is assigned to the user in an organization with the voice service.

GET /api/v1/voice/sounds

Audio prompts of the organization, with the places that use them.

Reading with voice.use; uploading, renaming and deleting need voice.routing.

Access: permission voice.use in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Query parameters

Name Description
kind ivr (messages of the flows), greeting (voicemail greetings) or system (messages the platform plays; the organization's own and, as inherited, the ones of its domain) (optional).

Response: sounds: List of sounds {id, name, slug, kind, filename, extension, size, duration_s, used_by [{type flow|group|number, id, name}], attached_to, key and language (system)}. limits: The limits of the domain {max_per_account, min_duration_s, max_duration_s, max_bytes, formats}.

POST /api/v1/voice/sounds

Upload an audio prompt (multipart/form-data). The limits of the domain are checked here: count, formats, size and the duration measured from the file (defaults: 20 sounds, wav/mp3/m4a/ogg/webm, 5 MB, 1 to 90 seconds). Refused uploads answer 400 with a code (format_invalid, empty_file, file_too_large, audio_unreadable, duration_too_short, duration_too_long, account_limit).

A system sound replaces the one of the organization with the same key and language.

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
file The audio file.
name Name of the sound (default: the file name).
kind ivr (default), greeting or system.
key System sound only: filtering (announcement before a filtered call), name_prompt (asks the caller's name), invalid_key (wrong key in a menu), voicemail_greeting (default voicemail message), paused (number paused or asleep) or recording (legal announcement of a recorded call).
language System sound only: fr, en or es.

Response: sound: The created sound, with duration_s.

DELETE /api/v1/voice/sounds/<sound_id>

Delete a sound. Refused (409 conflict with used_by) while a flow, a group or the routing of a number (its message, playback or dial-then-playback) uses it; the check comes first, nothing is touched when it fails. The numbers that play it as their voicemail greeting lose it (voicemail.sound_id becomes null).

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
sound_id Sound ID.

Response: success: True.

PUT /api/v1/voice/sounds/<sound_id>

Rename a sound.

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
sound_id Sound ID.

Request body

Name Description
name New name.

Response: sound: The updated sound.

PUT /api/v1/voice/sounds/<sound_id>/attach

Attach a voicemail greeting to numbers (the full set; a number has one greeting, so it leaves the others).

The greeting becomes the voicemail.sound_id of each number (the same fact as PATCH /voice/numbers/<id>), and the numbers that leave the set lose it.

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
sound_id Sound ID (kind greeting).

Request body

Name Description
phone_number_ids Voice numbers of the organization that play this greeting (empty list: none).

Response: sound: The sound with attached_to and used_by.

GET /api/v1/voice/sounds/<sound_id>/file

The uploaded audio file, to listen to it in the portal (the system sounds of the domain too).

Readable with voice.use, like the list of sounds.

Access: permission voice.use in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
sound_id Sound ID.

Response: The file (audio/*).

GET /api/v1/voice/sounds/tts

What text-to-speech can do for the sounds (voices of the platform's Piper service).

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: available (boolean): Whether text-to-speech is offered (false: the portal hides it). languages: Languages it speaks (fr, en, es; only those whose voice is installed). max_length: Longest text accepted, in characters.

POST /api/v1/voice/sounds/tts

Create a sound from a text (text-to-speech, Piper voices). The sound counts like an uploaded one (same limits of the domain: count, size, duration) and keeps its text (tts).

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
text Text to speak (required, 1000 characters at most).
language fr, en or es (required; one of languages of GET /voice/sounds/tts).
name Name of the sound (default: the start of the text).
kind ivr (default) or greeting.

Response: sound: The created sound, with duration_s and tts {text, language}.

Errors 400 validation_error: text missing or too long (field text), unknown language (field language), kind (field kind). 400 with code as POST /voice/sounds (duration_too_long, account_limit...). 503 tts_unavailable: the text-to-speech service is off or failed.

GET /api/v1/voice/stats

Call statistics of the organization: totals and one row per number (dashboard, numbers list).

Needs the voxos capability (403 voice_not_enabled otherwise). Aggregated on the call records (CDR) of the organization, blocked calls included in calls. answered is the calls answered, missed those neither answered nor blocked by the blacklist, duration the billable seconds of the answered calls, cost the sum of the cost of the rated calls (a call not rated yet counts for 0). Without voice.calls.all, only the calls of the user's own SIP endpoints.

Access: permission voice.use in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Query parameters

Name Description
from Calls started at or after this ISO 8601 date or datetime (optional; no time zone means UTC).
to Calls started at or before this ISO 8601 datetime; a date alone (2026-10-31) covers that whole day (optional).
number_id Only the calls of this phone number (optional).
direction "inbound" or "outbound" (aliases "in" and "out"; optional).
call_type direct, flow, forward, voicemail, screened, blocked or callcode (optional).

Response: stats: totals ({calls, answered, missed, duration, cost}), by_number (list of {phone_number_id, number, calls, missed, duration, cost}, the busiest number first; calls without number are listed with null), currency (CAD unless the domain sets another one for voice billing).

GET /api/v1/voice/system-prompts

Standard prompts of the voice server that a flow can play (play and menu steps, system_sound).

Installed in French and English on every voice server. A step plays each one in the language of the call (the flow's, or the one the caller switched to), or in a fixed language with fr: or en: before the name ("en:for-english-press" in a French menu), or only in some languages of the call ({name, only: ["fr"]}); up to 6 in a row. GET /voice/system-prompts/<name>/audio plays one.

Access: permission voice.use in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: prompts: [{name, group (greeting, menu, keys, digits, waiting, info, voicemail, errors, end), text {fr, en}}]. languages: Languages they exist in. max: Most prompts in one step.

GET /api/v1/voice/system-prompts/<path:name>/audio

Listen to a standard prompt of GET /voice/system-prompts (the IVR editor plays it before choosing it).

Access: permission voice.use in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
name Name of a prompt of the catalog (without fr: or en:), e.g. if-u-know-ext-dial or digits/9.

Query parameters

Name Description
language fr (default) or en.

Response: The prompt as a WAV file (audio/wav, 8 kHz mono), kept an hour in the browser.

Errors 404 not_found: not a prompt of the catalog, or no voice server gave it. 400 validation_error: unknown language (field language).

Number voice features

Pause, sleep mode, voicemail, call recording and limits per number. voxos capability.

GET /api/v1/voice/calls/<call_id>/recording

The recording of a call (MP3). Permission voice.recordings only (the Admin profile): a recording is a private conversation.

Answers 200 with the audio; 202 {status: "processing", retry_after} while the file is being prepared (try again after retry_after seconds); 410 recording_expired once the retention is over or after a deletion; 404 not_found for an unknown call, a call that was not recorded and a recording that was lost.

Access: permission voice.recordings in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
call_id Call ID.

Query parameters

Name Description
download 1 to get it as an attachment (default: inline).

Response: The MP3 (audio/mpeg).

GET /api/v1/voice/limits

The limits that apply to the organization's voice service.

Set by the domain administrator (PUT /domains/<id>/organizations/<organization_id>/voice-limits, PUT /domains/<id>/voice-limits); with the phone system of a subscription, max_channels follows its lines.

Access: permission voice.use in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: limits: {max_channels (simultaneous calls, a forward counts two; null: no limit), source (subscription: the lines of the phone system, organization, formula, domain or none), forward_countries (ISO codes a forward or an outbound call may go to), allowed_prefixes (extra destinations allowed), premium_blocked, max_call_seconds, max_call_cost, spend_limit, spend_window_minutes, suspend_on_cap, international_hours, loop_max_attempts, loop_window_seconds, unknown_rate_per_minute}. outbound_suspension: Why the outbound calls of the organization are suspended {reason (spend, call_cost, manual, platform: the switch of the whole platform), detail, at}; null when they are not. Only a domain or platform administrator lifts it (POST /domains/<id>/organizations/<organization_id>/voice-outbound/resume; the platform switch: PUT /system/voice-security).

GET /api/v1/voice/mailboxes

The voicemail boxes of the endpoints and the standalone boxes the user listens to: the box of their own endpoints and the standalone boxes they are a member of (all of them with voice.routing).

The boxes of the numbers are the voice settings of each number (GET /voice/numbers, PATCH /voice/numbers/<id>).

Access: permission voice.use in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: enabled (boolean): Whether the organization has the voicemail_boxes capability (no box otherwise). mailboxes: [{id, kind (endpoint or standalone), name, enabled, endpoint_id, endpoint {id, label, user_id}, greeting_sound_id, language, emails, delay, member_ids, unread}].

POST /api/v1/voice/mailboxes

Create the box of an endpoint or a standalone box (capability voicemail_boxes).

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
kind endpoint (the box of an endpoint: it takes the message when the routing rings that endpoint and nobody answers) or standalone (a box chosen in a call flow) (required).
endpoint_id The endpoint (kind endpoint; one box per endpoint).
name 1 to 60 characters (standalone: required; endpoint: default the endpoint's label).
enabled Default true.
greeting_sound_id A sound of the organization played before the beep (empty: the system greeting).
language fr, en or es (default fr).
emails Up to 10 addresses that receive each message (MP3 attached).
delay Seconds the endpoint rings before the box answers, 1 to 240 (kind endpoint, default 20).
member_ids Users who listen to it (standalone; the members who have voice.routing always do).

Response: mailbox: The box.

Errors 403 capability_required: the organization does not have voicemail_boxes.

DELETE /api/v1/voice/mailboxes/<mailbox_id>

Delete a box. Its messages are kept (only the members who have voice.routing see them afterwards); a call flow that named it says that nobody is available.

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
mailbox_id Box ID.

Response: success: True.

PUT /api/v1/voice/mailboxes/<mailbox_id>

Change a box (the fields given; see POST, kind and endpoint_id cannot change).

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
mailbox_id Box ID.

Response: mailbox: The box.

PATCH /api/v1/voice/numbers/<phone_id>

Change the voice functions of a number: pause, sleep mode, voicemail, call recording, name as caller ID.

The fields given are changed (voicemail and recording field by field). A paused number (enabled: false) plays a message and hangs up; a number in sleep mode goes to the voicemail when it is on, else plays the same message, and sends no SMS push. Pausing a number takes it out of sleep mode. Sleep mode is refused (409 function_not_allowed) on a paused number and on a number routed to a call flow. Switching the voicemail off removes a routing that points at it. A number in the trash answers 409 conflict.

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
phone_id Phone number ID (voice-enabled, in the organization).

Request body

Name Description
enabled false pauses the number (calls get a message), true resumes it. A pause of the balance lifecycle (paused_reason: balance) is not changed by this.
sleep true puts the number in sleep mode, false takes it out.
use_label_as_caller_id Present the number's name as the caller name on outgoing calls (needs a name).
voicemail {enabled, delay (ring seconds before the voicemail, 1 to 240), emails (at most 10 addresses that receive the message as an MP3), sound_id (own greeting, a sound of the organization, null for the system message), language (fr, en or es)}.
recording {enabled (record the calls of this number), announce (play the legal announcement; null follows the domain)}.

Response: number: The number with state {enabled, sleep}, features {voicemail, recording} and routing.

POST /api/v1/voice/numbers/sleep

Put several numbers in sleep mode, or take them out, in one request (all or none).

Access: permission voice.routing in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
number_ids IDs of the numbers (voice-enabled, in the organization), 1 to 100.
sleep true to put them in sleep mode, false to take them out.

Response: numbers: The numbers after the change. 409 function_not_allowed (with number_id) when one of them is paused or routed to a call flow; none is changed then.

GET /api/v1/voice/rating/max-duration

The longest call to a destination a credit pays for, at the organization's retail price (increments, minimum and connection fee included), capped by the domain's longest call.

Access: permission voice.use in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Query parameters

Name Description
destination The number called (required).
credit The amount to spend (default: the organization's balance).

Response: seconds: The longest call (0: the credit does not pay for a call). credit: The credit used. rate: The rate applied, or null when no price covers the destination (then seconds is 0 and reason no_rate). reason: null, no_rate or insufficient_balance.

GET /api/v1/voice/rating/quote

What a call to a destination is billed to the organization (its retail price), from the cost of the trunk its outbound rules choose and the margins. The platform and domain administrators also see the wholesale price; the platform administrator the cost.

Access: permission voice.use in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Query parameters

Name Description
destination The number called (required).
seconds Length of the call (default 60).

Response: quote: {destination, prefix, seconds, retail {per_minute, amount, billed_seconds, rules, mode} or null (the domain bills with its own price list: see rate)}, or null when no cost covers the destination. rate: The rate that applies to the organization (as the domain's price list or the margin gives it): {rate_per_min, connection_fee, increment, min_seconds, currency, source} or null.

DELETE /api/v1/voice/recordings

Delete the recordings of some calls (the call records stay).

Access: permission voice.recordings in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
call_ids IDs of the calls, 1 to 100. Calls of other organizations and calls without a recording are ignored.

Response: deleted: IDs of the calls whose recording was deleted.

GET /api/v1/voice/voicemails

Voicemail messages of the organization, newest first.

Only the messages the user may hear (the rule of every /voice/voicemails route and of the push of a new message). The box of a number (mailbox_id null): the members who have voice.calls.all or voice.routing, and a member whose SIP endpoint the routing of that number rings, directly, through a ring group (a stage or its fallback) or through a call flow (a dial or groupdial step, or a goto to such a flow: a number behind an IVR counts). The box of an endpoint: the user of the endpoint, and the members who have voice.routing. A standalone box: its members (member_ids), and the members who have voice.routing. A box deleted since: the members who have voice.routing. The platform and domain administrators hear every message.

Access: permission voice.use in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Query parameters

Name Description
number_id Only the messages of this number (optional).
mailbox_id Only the messages of this box (an id of GET /voice/mailboxes), or "numbers" for the boxes of the numbers (optional).
listened true for the listened messages, false for the new ones (optional).
skip Pagination offset (default 0).
limit Page size (default 50, at most 100).

Response: voicemails: The page: id, phone_number_id, mailbox_id (null: the box of the number), number, caller, caller_name, duration (seconds), size, language, listened_at, created_at, expires_at. total: Number of messages matching the filters. skip: Offset applied. limit: Page size applied.

DELETE /api/v1/voice/voicemails/<voicemail_id>

Delete a voicemail message and its audio.

A member who has voice.routing deletes any message; a listener of the box of an endpoint or of a standalone box (the rule of GET /voice/voicemails) deletes its messages; the messages of the box of a number need voice.routing (403 forbidden otherwise, voice.calls.all is not enough). A message the user may not hear: 404 not_found.

Access: permission voice.use in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
voicemail_id Voicemail ID.

Response: success: True.

PUT /api/v1/voice/voicemails/<voicemail_id>

Mark a voicemail message listened or new.

Only a message the user may hear (the rule of GET /voice/voicemails); 404 not_found otherwise.

Access: permission voice.use in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
voicemail_id Voicemail ID.

Request body

Name Description
listened true marks it listened (the first time is kept), false marks it new.

Response: voicemail: The message.

GET /api/v1/voice/voicemails/<voicemail_id>/file

The audio of a voicemail message (MP3).

Only a message the user may hear (the rule of GET /voice/voicemails); 404 not_found otherwise.

Access: permission voice.use in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
voicemail_id Voicemail ID.

Query parameters

Name Description
download 1 to get it as an attachment (default: inline).

Response: The MP3 (audio/mpeg).

Domain voice settings

Retention of recordings and messages, channel and destination limits (domain admin).

GET /api/v1/domains/<domain_id>/organizations/<organization_id>/voice-limits

Voice limits of an organization: its own overrides and what applies after the domain's defaults.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
organization_id Organization ID.

Response: voice_limits: The organization's overrides {max_channels, forward_countries, allowed_prefixes, max_call_seconds, max_call_cost, spend_limit, spend_window_minutes, suspend_on_cap, international_hours} (null: the domain's value). effective: What applies: {max_channels, source (subscription: the lines of the phone system, organization, formula, domain or none), forward_countries, allowed_prefixes, premium_blocked} and the fraud guards (the organization's value, else the domain's). suspension: Why the outbound calls of the organization are suspended {reason (spend, call_cost, manual, platform: the switch of the whole platform, which comes first), detail, at, by (ID of the administrator for manual, "platform" for platform)}, null when they are not.

PUT /api/v1/domains/<domain_id>/organizations/<organization_id>/voice-limits

Change the voice limits of an organization (the fields given are changed, null removes an override). Journalled.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
organization_id Organization ID.

Request body

Name Description
max_channels Simultaneous calls allowed, 1 to 10000 (null: the domain's rule, or the lines of the phone system when the organization has it). A forward counts two. A value set here wins over the lines of the phone system until it is cleared.
forward_countries ISO codes a forward may go to (null: the domain's list).
allowed_prefixes Digit prefixes (country code first) always allowed as a destination, at most 100 (null: none).
max_call_seconds As the domain's (PUT /domains/<id>/voice-limits); null: the domain's value.
max_call_cost As the domain's (PUT /domains/<id>/voice-limits); null: the domain's value.
spend_limit As the domain's (PUT /domains/<id>/voice-limits); null: the domain's value.
spend_window_minutes As the domain's (PUT /domains/<id>/voice-limits); null: the domain's value.
suspend_on_cap As the domain's (PUT /domains/<id>/voice-limits); null: the domain's value.
international_hours As the domain's (PUT /domains/<id>/voice-limits); null: the domain's value.

Response: voice_limits: The organization's overrides after the change. effective: What applies.

GET /api/v1/domains/<domain_id>/organizations/<organization_id>/voice-outbound

Outbound call rules of an organization: default trunk and rules on the destination (see services/voice_outbound.py).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
organization_id Organization ID.

Query parameters

Name Description
destination A number to test (E.164, or 10 digits for North America): test tells which trunk the rules choose.

Response: voice_outbound: {default_trunk_id, rules: [{pattern, trunk_id, label}]} (rules applied in order, the first whose regular expression matches the destination in E.164, e.g. +15145551234, wins; then the default trunk; then the former order: the number's trunk, the trunk of arrival, a shared trunk). trunks: The trunks it may choose [{id, label, scope domain|platform, outbound_enabled, enabled}]; a platform trunk is named by its description (and its name for a super admin). test: With destination: {destination (E.164), trunk_id (null: the rules say nothing)}.

PUT /api/v1/domains/<domain_id>/organizations/<organization_id>/voice-outbound

Replace the outbound call rules of an organization. Journalled.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.
organization_id Organization ID.

Request body

Name Description
default_trunk_id Trunk for every outbound call no rule matches (null: the former order).
rules Ordered [{pattern, trunk_id, label}] (50 at most): pattern = Python regular expression (1 to 100 characters) searched in the destination in E.164, e.g. "^+1(514|438)" or "^+33"; trunk_id = a trunk of trunks that allows outbound calls; label = free text (80 characters).

Response: voice_outbound: The rules now in force. trunks: As GET.

Errors 400 validation_error (field default_trunk_id or rules): invalid regular expression, trunk the organization may not use or without outbound calls.

POST /api/v1/domains/<domain_id>/organizations/<organization_id>/voice-outbound/resume

Resume the outbound calls of an organization suspended by the fraud guards (spending over the window, a call cut at its maximum cost) or by an administrator. The organization itself cannot do it. Journalled with the suspension it lifts.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
organization_id Organization ID.

Request body

Name Description
note What was checked (optional string, kept in the journal).

Response: suspension: null.

POST /api/v1/domains/<domain_id>/organizations/<organization_id>/voice-outbound/suspend

Suspend the outbound calls of an organization (calls of its endpoints, its forwards); the calls in progress are hung up within seconds, incoming calls go on. Journalled; the administrators receive an alert.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
organization_id Organization ID.

Request body

Name Description
note Why (optional string, kept up to 500 characters).

Response: suspension: {reason: manual, detail {note}, at, by (ID of the administrator)}.

GET /api/v1/domains/<domain_id>/organizations/<organization_id>/voice-recording

Recording rules of the outbound calls of the endpoints of an organization (they win over the endpoint's own setting; see services/voice_recording.py).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
organization_id Organization ID.

Response: voice_recording: {outbound_always (every outbound call of its endpoints is recorded), outbound_follows_number (the outbound calls presenting a number whose recording is on are recorded), recording_skip_allowed (a user may skip the imposed recording of his next outbound call, from the app)}.

PUT /api/v1/domains/<domain_id>/organizations/<organization_id>/voice-recording

Change the recording rules of the outbound calls of an organization (the fields given; the others are kept). Journalled.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
organization_id Organization ID.

Request body

Name Description
outbound_always Record every outbound call of its endpoints.
outbound_follows_number Record the outbound calls that present a number whose recording is on.
recording_skip_allowed A user may skip the imposed recording of his next outbound call (PUT /voice/call-info/recording/skip-next).

Response: voice_recording: The rules now in force, as GET.

GET /api/v1/domains/<domain_id>/voice-limits

Voice limits of a domain: channels, destinations and the fraud guards of outbound calls (E47). By default an organization has 10 simultaneous calls, calls to the public network (endpoints, forwards, external members of a group) go to Canada and the United States only, premium-rate numbers are refused, a call is cut after 4 hours or at 10 of cost, and 10 of cost in 5 minutes suspends the outbound calls of the organization. The voice server applies them.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: voice_limits: {default_max_channels (10 by default), channels_per_destination, min_channels, forward_countries, premium_blocked, max_call_seconds, max_call_cost, spend_limit, spend_window_minutes, suspend_on_cap, international_hours, loop_max_attempts, loop_window_seconds, unknown_rate_per_minute} (meaning and defaults: see PUT).

PUT /api/v1/domains/<domain_id>/voice-limits

Change the voice limits of a domain (the fields given are changed).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
default_max_channels Simultaneous calls of an organization without its own limit, 1 to 10000 (default 10; null: none). A forward counts two.
channels_per_destination Formula by forward destinations: the limit is max(min_channels, distinct forward destinations x this) (null: off).
min_channels Floor of the formula (default 2).
forward_countries ISO 3166-1 alpha-2 codes a forward, a call of an endpoint or a call code may go to (default US and CA).
premium_blocked Refuse premium-rate numbers (900, 976; default true).
max_call_seconds Longest outbound call, 60 to 86400 seconds (default 14400).
max_call_cost The platform's cost (CAD, from the cost table of the trunk) at which an outbound call is cut, 0.5 to 10000 (default 10).
spend_limit The platform's cost (CAD) over the window that suspends the outbound calls of an organization, 0.5 to 100000 (default 10).
spend_window_minutes The sliding window of spend_limit, 1 to 1440 minutes (default 5).
suspend_on_cap A call cut at max_call_cost suspends the outbound calls of the organization (default true).
international_hours Business hours {timezone, schedule {mon..sun: [[start, end]]}, exceptions, closures} outside which a call abroad (outside Canada and the United States) is refused; null: any time (default).
loop_max_attempts The same caller to the same number more often than this in loop_window_seconds is a loop, 1 to 100 (default 3).
loop_window_seconds 10 to 3600 (default 60).
unknown_rate_per_minute Cost assumed for a destination in Canada or the United States that no cost table covers, 0 to 10 (default 0.05); elsewhere such a call is refused.

Response: voice_limits: The limits after the change.

GET /api/v1/domains/<domain_id>/voice-pricing

How the calls of the domain are priced: the platform's margin (what the domain is billed) and the domain's own (what its customers are billed).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: voice_pricing: {wholesale (the rules of the price billed to the domain: markup_percent, min_markup_per_minute, min_billed_seconds, increment_seconds, connection_fee), wholesale_override (super admin only: the rules set for this domain over the platform's), retail {mode (margin: the domain's margin over the wholesale price; rates: the domain's own price list, GET /domains/<id>/voice-rates), markup_percent, min_markup_per_minute, min_billed_seconds, increment_seconds, connection_fee}}.

PUT /api/v1/domains/<domain_id>/voice-pricing

Change the pricing of the domain's calls.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
retail {mode (margin or rates), markup_percent, min_markup_per_minute, min_billed_seconds, increment_seconds, connection_fee}: the domain's margin (bounds as PUT /system/voice-pricing).
wholesale The platform's margin for this domain (super admin only, else 400 validation_error): the same rules; a rule set to null goes back to the platform's.

Response: voice_pricing: As GET.

GET /api/v1/domains/<domain_id>/voice-rating/quote

What a call to a destination costs and is billed, for an organization of the domain (or through a trunk).

The cost and the provider's table are shown to the super admin only.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Query parameters

Name Description
destination The number called (required).
seconds Length of the call (default 60).
organization_id The organization calling (its outbound rules choose the trunk).
trunk_id A trunk the domain may use, instead.

Response: quote: {destination, prefix, seconds, wholesale {per_minute, amount, billed_seconds, rules}, retail ({per_minute, amount, billed_seconds, rules, mode} or null: the domain's own price list applies), trunk_id; super admin: cost {per_minute, currency, amount, source_per_minute, source_currency}, trunk_name, table_id, table_name, description}, or null when no cost covers the destination.

GET /api/v1/domains/<domain_id>/voice-security

Voice security of a domain: its organizations whose outbound calls are suspended and their latest security events.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: platform_suspended (boolean): Every outbound call of the platform is suspended. suspended: [{organization_id, name, suspension {reason, detail, at, by}}]. events: The latest 100 security events of the domain's organizations, newest first [{id, organization_id, organization_name, type (outbound_suspended, call_cost_reached, call_duration_reached, refused_destination, refused_international_hours, refused_loop, refused_no_rate, refused_source, new_country, endpoint_locked, night_volume...), detail, at, notified (an alert was sent)}].

GET /api/v1/domains/<domain_id>/voice-settings

Voice settings of a domain: retention of recordings and voicemail messages, voicemail recording limits (E27, E31).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: voice_settings: {recording_days (days a recording is kept, 0: forever; default 30), recording_announce (play the legal announcement on recorded calls unless the number says otherwise; default false), voicemail {retention_days (default 90, 0: forever), max_seconds (longest message, default 180), silence_seconds (silence that ends a message, default 5), min_seconds (shorter messages are dropped, default 1)}, auto_softphone (every organization of the domain gets a SIP endpoint for the softphone of the mobile app and its voice numbers ring it; default false)}.

PUT /api/v1/domains/<domain_id>/voice-settings

Change the voice settings of a domain (the fields given are changed). New recordings and messages use them; those already stored keep their expiry.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
recording_days Days a recording is kept, 0 to 3650 (0: forever).
recording_announce Play the legal announcement on recorded calls (boolean).
voicemail {retention_days 0-3650, max_seconds 10-600, silence_seconds 2-30, min_seconds 0-10}.
auto_softphone Automatic softphone. An organization created in the domain from then on (sign-up, OAuth sign-up, creation by an admin) receives the capabilities voxos and sip of the domain and a SIP endpoint for its owner, once they hold a membership in it (label: their name; the calls it does not answer go to the owner's verified phone when they have one); a voice number the organization receives without routing (purchase, assignment by the domain admin, port completed) rings that endpoint, with the push of the mobile app, and becomes the number its calls present. Existing organizations: the operator's catch-up command. Turning it off changes nothing already created.

Response: voice_settings: The settings after the change.

Voice (internal)

Deposit of voicemail messages and recordings by the voice servers, service token. Not for users or API keys.

GET /api/v1/internal/voice/ping

Check the service token (the voice server calls it at start-up).

Access: service token (Authorization: Bearer $VOICE_INTERNAL_TOKEN), neither a user nor an API key
API keys: refused, 403 forbidden (API key not allowed on this route)

Response: ok: True.

Errors 401 unauthorized: the Authorization: Bearer header is missing or is not the service token. 503 internal_api_disabled: VOICE_INTERNAL_TOKEN is not set on the server.

POST /api/v1/internal/voice/recordings

Deposit the recording of a call (service token). The call record must exist: the voice server retries on 404 call_not_found.

Access: service token (Authorization: Bearer $VOICE_INTERNAL_TOKEN), neither a user nor an API key
API keys: refused, 403 forbidden (API key not allowed on this route)

Request body

Name Description
file The recording, MP3 (multipart, 30 MB at most).
call_uniqueid Unique id of the call on the voice server (required).
duration Length in seconds (optional).

Response: recording: {state, available_at, expires_at, size, duration}. 201 when stored, 200 when this call already has its recording.

Errors 401 unauthorized: the Authorization: Bearer header is missing or is not the service token. 503 internal_api_disabled: VOICE_INTERNAL_TOKEN is not set on the server.

POST /api/v1/internal/voice/voicemails

Deposit a voicemail message recorded by a voice server (service token).

Depositing the same call_uniqueid again changes nothing and answers 200 with the stored message, so the voice server can retry safely.

Access: service token (Authorization: Bearer $VOICE_INTERNAL_TOKEN), neither a user nor an API key
API keys: refused, 403 forbidden (API key not allowed on this route)

Request body

Name Description
file The message, MP3 (multipart, 4 MB at most).
phone_number_id ID of the number whose box it is (required; an unknown or unassigned number is 404).
caller Caller ID as the carrier gave it.
caller_name Caller name (optional).
duration Length in seconds (required).
call_uniqueid Unique id of the call on the voice server (makes the deposit idempotent).
language fr, en or es (optional; default: the language of the box).
mailbox_id The box of an endpoint or the standalone box the message was left in (optional; it must belong to the organization of the number, else 404); absent: the box of the number.
recorded_at When the message was left, ISO 8601 UTC (optional; default now).

Response: voicemail: The stored message. 201 when new, 200 when this call's message was already deposited.

Errors 401 unauthorized: the Authorization: Bearer header is missing or is not the service token. 503 internal_api_disabled: VOICE_INTERNAL_TOKEN is not set on the server.

Metrics

Volumes, performance, export.

GET /api/v1/metrics/agents

Get list of agents with message counts.

Integrations (hidden service accounts) are not agents and are not listed; the messages they send still count in the team totals.

Access: permission metrics.all or metrics.team in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Query parameters

Name Description
team_id Team ID or comma-separated list of team IDs (optional; default: every team the caller can access, only the team they are placed in with metrics.team alone; a team outside them gets 403 forbidden).
start_date Start date ISO format.
end_date End date ISO format.
days Number of days to look back (default 30).

Response: agents: The agents with their message counts (integrations are not listed). period: {start, end} (ISO 8601) of the period.

GET /api/v1/metrics/export

Export metrics data as JSON.

Access: permission view_global_metrics (now metrics.all)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Query parameters

Name Description
team_id Team ID (optional; default: every team the caller can access in the current domain; a team outside them gets 403 forbidden).
start_date Start date ISO format.
end_date End date ISO format.
days Number of days to look back (default 30).

Response: Comprehensive metrics data for export.

GET /api/v1/metrics/overview

Get overview metrics.

Access: permission metrics.all or metrics.team in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Query parameters

Name Description
team_id Team ID or comma-separated list of team IDs (optional; default: every team the caller can access in the current domain, only the team they are placed in with metrics.team alone; a team outside them gets 403 forbidden).
user_id User ID or comma-separated list of user IDs (optional, filters by sent_by).
start_date Start date ISO format.
end_date End date ISO format.
days Number of days to look back (default 30).

Response: Overview metrics.

GET /api/v1/metrics/performance

Get performance metrics (response times, delivery rates).

Access: permission metrics.all or metrics.team in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Query parameters

Name Description
team_id Team ID (optional; default: every team the caller can access in the current domain, only the team they are placed in with metrics.team alone; a team outside them gets 403 forbidden).
start_date Start date ISO format.
end_date End date ISO format.
days Number of days to look back (default 30).

Response: period: {start, end} (ISO 8601) of the period. delivery_rate: Share of the messages delivered. messages_by_status: Number of messages per status. response_time: Response time statistics.

GET /api/v1/metrics/team/<team_id>

Get detailed metrics for a specific team.

by_user lists the agents only: integrations (hidden service accounts) are left out, their messages still count in the overview. With metrics.team alone (no metrics.all), only the team the caller is placed in (403 forbidden for another one).

Access: permission metrics.all or metrics.team in the organization (profiles); team access (team_id)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
team_id Team ID.

Query parameters

Name Description
start_date Start date ISO format.
end_date End date ISO format.
days Number of days to look back (default 30).

Response: Detailed team metrics.

GET /api/v1/metrics/user/<user_id>

Get metrics for a specific user.

The messages counted are those sent in the teams the caller can access (a user who writes in several organizations shows each caller only what happened in theirs; a platform_admin sees all). One's own metrics: metrics.team is enough (counted in the team one is placed in); another user's: metrics.all and the right to manage that user (403 forbidden otherwise).

Access: permission metrics.all or metrics.team in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
user_id User ID.

Query parameters

Name Description
start_date Start date ISO format.
end_date End date ISO format.
days Number of days to look back (default 30).

Response: User metrics.

GET /api/v1/metrics/volume

Get message volume metrics by time period.

Access: permission metrics.all or metrics.team in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Query parameters

Name Description
team_id Team ID or comma-separated list of team IDs (optional; default: every team the caller can access in the current domain, only the team they are placed in with metrics.team alone; a team outside them gets 403 forbidden).
user_id User ID or comma-separated list of user IDs (optional, filters by sent_by).
start_date Start date ISO format.
end_date End date ISO format.
days Number of days to look back (default 30).
granularity hour, day, week, or month (default day).
group_by_team If "true", group results by team (default false).
group_by_user If "true", group results by user (default false); integrations (service accounts) get no row of their own.

Response: Volume data by period.

Billing

Credits, usage, invoices, pricing plans, admin operations.

GET /api/v1/billing

Billing overview of the current organization.

Not available to restricted API keys (a key limited to a team, or an integration's key): 403 with error forbidden.

Errors are {error, message}: 400 no_organization when the user has no organization.

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: billing: Billing record (credit_balance, auto_recharge, payment_method without secrets, billing_email, pricing_plan_id, custom_rates, and the negative-balance lifecycle of E46: balance_state ok, negative_grace, suspended, limbo or released, with negative_since, grace_ends_at, suspended_at, limbo_at, release_at, released_at in UTC ISO 8601 with Z, null when they do not apply). pricing_plan: Pricing plan in force (rates, volume discounts). current_month_usage: Usage of the billing period by type, with total_cost and total_messages. billing_period: start and end of the current billing period. recent_transactions: The latest credit transactions.

POST /api/v1/billing/admin/add-credits

Add credits to an organization by hand (super admin only). Errors are {error, message}: 400 validation_error with field (organization_id missing, amount not above zero).

Access: platform permission platform_admin
API keys: refused, 403 forbidden (API key not allowed on this route)

Request body

Name Description
organization_id Organization ID (required).
amount Credits to add, above zero (required).
description Label of the transaction.

Response: success: true. new_balance: Balance after the credit.

POST /api/v1/billing/admin/assign-plan

Assign a pricing plan to an organization, with optional custom rates (super admin only).

Errors are {error, message}: 400 validation_error (organization_id missing), 404 not_found (plan), 500 internal_error.

Access: platform permission platform_admin
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
organization_id Organization ID (required).
pricing_plan_id Plan ID (null: back to the default plan).
custom_rates Rates that replace those of the plan for this organization (null: none).

Response: Same response as GET /billing for the organization.

POST /api/v1/billing/admin/charge-dids

Charge monthly DID fees for all organizations (super admin only).

This should be called at the beginning of each month. Only DIDs not yet billed this month will be charged. An organization whose domain charges its monthly fees itself (settings.billing.monthly_fees, E40) is charged by that run instead.

Access: platform permission platform_admin
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: organizations_processed: Number of organizations listed in results. results: One row per organization charged something or in error: {organization_id, organization_name, charged_count, total_cost, charged_dids} or {organization_id, organization_name, error}.

POST /api/v1/billing/admin/fix-usage-dates

Give each usage record the date of its message (super admin only, maintenance).

Access: platform permission platform_admin
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: success: true. fixed_count: Number of records changed.

POST /api/v1/billing/admin/mark-billed

Mark usage records as billed (super admin only).

Access: platform permission platform_admin
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
organization_id Organization ID (required).
record_ids IDs of the records to mark.
usage_type Without record_ids, marks every record of this type for the current month.

Response: success: true. marked_count: Number of records marked.

PUT /api/v1/billing/auto-recharge

Change the auto-recharge settings. When the balance is at or under threshold, the card is charged: mode fixed buys amount credits each time, top_up_to buys what brings the balance up to amount. Turning it on with a balance already at or under the threshold recharges at once. A failed attempt is remembered (last_failure_at): no new attempt for 24 hours unless the card or the settings change, and the members who hold billing.manage are told by e-mail, SMS, push and the real-time event auto_recharge_failed.

Errors are {error, message}: 400 validation_error with field (enabled not a boolean, threshold below zero, not a number or not among the domain's choices, amount not above zero or under the minimum or over the balance cap, mode, payment_method_id missing or not a card of the organization, amount not above threshold in top_up_to mode).

Access: permission billing.manage in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
enabled Whether the balance is recharged automatically.
threshold Balance at or under which a recharge starts (default 20, at least 0).
amount Credits bought by each recharge (fixed) or balance to reach (top_up_to) (default 100, above zero).
mode fixed (default; the previous behavior) or top_up_to.
payment_method_id The card to charge (default: the main card).

Response: auto_recharge: The settings {enabled, threshold, amount, mode, payment_method_id, last_failure_at}.

GET /api/v1/billing/balance

Current credit balance of the organization (the real-time event balance_update announces each change).

Not available to restricted API keys (a key limited to a team, or an integration's key): 403 with error forbidden.

Errors are {error, message}: 400 no_organization when the user has no organization.

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: credit_balance: Balance in CAD (negative after a call rated after the fact, E46). auto_recharge: Auto-recharge settings {enabled, threshold, amount, mode, payment_method_id, last_failure_at}. balance_state: ok, negative_grace, suspended, limbo or released (E46); anything but ok comes with the dates below. negative_since: When the balance became negative (UTC, ISO 8601 with Z), or null. grace_ends_at: End of the grace period, when sending and calls are suspended, or null. suspended_at: When the account was suspended, or null. limbo_at: When the numbers were put in reserve, or null. release_at: When the numbers go back to the pool of the domain, or null. released_at: When they did, or null.

POST /api/v1/billing/check-balance

Check whether the balance covers a message, without sending or charging anything.

Not available to restricted API keys (a key limited to a team, or an integration's key): 403 with error forbidden.

Errors are {error, message}: 400 no_organization when the user has no organization.

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
body Text of the message (segments are counted from it).
has_media true for an MMS (default false).

Response: has_balance: Whether the balance covers the cost. estimated_cost: Cost of the message. current_balance: Current balance. segments: Number of SMS segments.

GET /api/v1/billing/invoices

Invoices of the organization, newest first.

Not available to restricted API keys (a key limited to a team, or an integration's key): 403 with error forbidden.

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Query parameters

Name Description
status Invoice status (draft, paid...).
skip Offset (default 0).
limit Page size (default 50, brought back into 1-100). A skip or limit that is not an integer is refused (400 validation_error with field).

Response: invoices: The page of invoices. total: Number of invoices matching the filters, whatever the page. skip: Offset applied. limit: Page size applied.

GET /api/v1/billing/invoices/<invoice_id>

One invoice of the organization.

Not available to restricted API keys (a key limited to a team, or an integration's key): 403 with error forbidden.

Errors are {error, message}: 404 not_found, 403 forbidden (invoice of another organization).

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
invoice_id Invoice ID.

Response: invoice: The invoice with its lines and taxes.

GET /api/v1/billing/overview

What the organization was billed and what it will be: the last statement, the next one estimated with each line explained, and the usage of the current period with what it already cost and a straight-line projection (an estimate).

Errors are {error, message}: 400 no_organization.

Access: permission billing.view or billing.manage in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: currency: Currency of the amounts. balance: Credit balance. kind: subscription (a plan or the phone system, statement each month), monthly_fees (monthly fees of the numbers) or usage_only (only what is used). lines: With the phone system {included, burst, limit, manual, max_channels, price, peak {lines, at}, overage, overage_cost} (the overage of the peak is in next), else null. period: The current period {start, end, elapsed (0 to 1)}: the subscription's month (anniversary to anniversary), else the calendar month. last_statement: null, or the last monthly statement {id, number, total, date, period_start, period_end}. next: The next statement {date, lines [{code, description, detail (why: what is counted, what is included), quantity, unit_price, amount}], total}: the subscription estimated with the seats of today (as GET /billing/subscription/estimate), the monthly fees of the numbers (one number_fees line per number), or no line and date null (usage_only: no statement is issued, each use is debited when it happens; as GET /domains/<id>/billing-overview). An organization counts as subscribed only while the domain offers subscriptions (as GET /billing/subscription). usage: The current period {spent {category: amount debited}, total_spent, communications (messages and calls debited), by_subtype {sms_out, sms_in, mms_out, mms_in, call, usage_other: {amount, count}}, messages null or {used, included, beyond, cost}, minutes null or {used, included, beyond, cost}}. projection: {usage (communications projected to the end of the period), total (next statement + projected usage)}; both null in the first day of the period. previous_month: Debits of the last calendar month {start, total}.

GET /api/v1/billing/payment-config

Public configuration of the payment providers of the domain (no secret key).

Not available to restricted API keys (a key limited to a team, or an integration's key): 403 with error forbidden.

Errors are {error, message}: 400 no_organization, 404 domain_not_found.

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: payment_providers: {stripe: {enabled, publishable_key}, nuvei: {enabled, merchant_id, merchant_site_id, environment}}, plus fake: {enabled} on a development or test domain that uses the test provider. default_provider: Provider to offer first (stripe or nuvei).

DELETE /api/v1/billing/payment-method

Remove the payment method of the organization and turn auto-recharge off.

Access: permission billing.manage in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: success: true.

POST /api/v1/billing/payment-method

Record the main payment method of the organization. Older name of POST /billing/payment-methods: for Nuvei the token is verified with the provider and brand, last four digits and expiry come from the provider; for Stripe (legacy) the data is kept as the client sent it.

Errors are {error, message}: 400 validation_error with field provider_payment_method_id when it is missing; for Nuvei also the errors of POST /billing/payment-methods.

Access: permission billing.manage in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
provider stripe (default) or nuvei.
provider_payment_method_id Identifier of the method at the provider (required).
card_type credit (default) or debit.
last4 Last four digits.
brand Card brand.
exp_month Expiry month, 1 to 12 (a whole number or a string of digits; 400 validation_error with field otherwise).
exp_year Expiry year, four digits (same rule).

Response: payment_method: {provider, last4, brand, exp_month, exp_year}.

GET /api/v1/billing/pricing-plans

The pricing plans the caller may see.

Pricing plans belong to the platform, and some are negotiated for one customer: a platform admin sees them all; a domain admin sees the plans in use in the organizations of their domains and the default plan (or, with X-Admin-Organization, the plan of that organization); any other caller sees the plan of their current organization (the default plan when it has none).

Access: permission billing.view in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: pricing_plans: List of plans with rates, volume discounts, included credits and monthly fee.

POST /api/v1/billing/pricing-plans

Create a pricing plan (super admin only).

Errors are {error, message}: 400 validation_error with field for a missing required field.

Access: platform permission platform_admin
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
name Internal name (required).
display_name Name shown to customers (required).
rates Rates by usage type (required).
volume_discounts Volume discount tiers.
included_credits Credits included each period.
monthly_fee Monthly fee.
is_default Whether this is the default plan.

Response: pricing_plan: The created plan.

DELETE /api/v1/billing/pricing-plans/<plan_id>

Delete a pricing plan (super admin only). Errors are {error, message}: 404 not_found, 409 conflict for the default plan.

Access: platform permission platform_admin
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
plan_id Pricing plan ID.

Response: success: true.

PUT /api/v1/billing/pricing-plans/<plan_id>

Update a pricing plan (super admin only). Errors are {error, message}: 404 not_found.

Access: platform permission platform_admin
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
plan_id Pricing plan ID.

Request body

Name Description
display_name Name shown to customers.
rates Rates by usage type.
volume_discounts Volume discount tiers.
included_credits Credits included each period.
monthly_fee Monthly fee.
is_default Whether this is the default plan.

Response: pricing_plan: The plan after the change.

POST /api/v1/billing/recharge

Recharge the balance: charge a card of the organization and credit the balance. The credits bought are amount; the taxes of the domain come on top of what the card is charged. A receipt (invoice) is issued and e-mailed. The balance is credited only after the provider approved the payment.

Send an Idempotency-Key header (or idempotency_key in the body): the same key never charges twice, it returns the first answer (replayed: true).

Errors are {error, message}: 400 validation_error (amount missing, not a number, more than two decimals or under the minimum, with field), 400 configuration_error (no payment method, payment provider not configured), 400 card_expired, 404 not_found (card of another organization), 409 billing_info_required (taxes apply and the billing address is missing), 409 quote_changed (with the new quote; also when the promotional code is no longer good at the moment the payment opens, quote.promo_info.error says why), 409 payment_in_progress (one payment at a time per organization, whatever the number of requests at once), 422 max_balance (with maximum), 402 payment_declined (with reason: card_declined, insufficient_funds, card_expired, cvv_declined or card_restricted), 402 authentication_required (the bank asks for a verification), 502 payment_unknown (the provider did not answer: nothing is credited and the payment is checked afterwards), 502 provider_error.

An unexpected failure answers 500 internal_error.

Access: permission billing.manage in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
amount Credits to buy, a number above zero with at most two decimals.
payment_method_id The card (default: the main card).
promo_code A promotional code (optional).
quote_total The total of the quote the customer saw; a different total is refused with quote_changed. It must be a finite number (400 validation_error with field quote_total otherwise).
idempotency_key Same as the Idempotency-Key header.

Response: success: true. new_balance: Balance after the recharge. transaction_id: Reference of the payment at the provider. transaction: The ledger row of the credit. invoice: {id, invoice_number} of the receipt. payment_id: The card debit. quote: What was charged {credits, promotion, subtotal, taxes, total, currency, credited}.

GET /api/v1/billing/subscription

The plans the domain offers and the subscription of the organization: plan, phone system, usage of the month, seats and next statement.

Errors are {error, message}: 400 no_organization.

Access: permission billing.view or billing.manage in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: enabled (boolean): The domain offers subscriptions. plans: The messaging plans [{key (essential, team, enterprise), name, price, included {messages, users, teams, numbers, widgets}, max_users, max_teams}]. voice: The phone system {price, included_numbers, full_user, virtual_user, minutes_per_full_user, minute_overage, warn_ratio, tollfree_inbound_per_minute, recording_per_extension, included_lines, lines_per_full_user, burst_lines, extra_line}. addons: Monthly price of the extras {number, tollfree_number, team, widget}. overage_tiers: Price of the messages beyond those included [{up_to, price}], by position in the month's volume (the last up_to is null). mms_weight: Messages an MMS counts for. subscription: null (no subscription, or the domain turned subscriptions off: then nothing of it is billed, as in GET /billing/overview), or {messaging_plan, voice, period_start, period_end, pending {messaging?, voice?} (changes at the next anniversary), usage {messages, messages_included, minutes, minutes_included}, seats {users, full_users, virtual_users, recording_extensions, numbers, tollfree_numbers, teams, widgets}, lines {included, burst, limit, manual, max_channels, price, peak {lines, at}, overage, overage_cost} (calls at once: included, allowed above, peak of the period and its overage billed on the next statement), next_statement {date, lines [{code, description, detail, quantity, unit_price, amount}], total}}.

PUT /api/v1/billing/subscription

Choose the plans of the organization. A first subscription starts today and its first month is charged at once; a dearer plan or the phone system added apply now (the difference charged pro rata of the month); a cheaper plan, a plan or the phone system dropped apply at the next anniversary. Everything is paid from the credit: the automatic recharge with a card must be on. Journaled.

Errors are {error, message}: 400 no_organization; 400 validation_error with field (field missing, unknown or inactive plan, voice not a boolean, subscriptions not offered by the domain, a first subscription with neither a plan nor the phone system); 402 auto_recharge_required (the automatic recharge is off and a plan or the phone system is chosen); 402 plan_limit {limit (users or teams), plan, upgrade (the first plan with room, or null), current (members who use the messages and pending invitations, or teams)}: the organization already has more than the chosen plan allows (checked when it is asked for, also for a cheaper plan that would only apply at the anniversary; keeping the plan held is never refused).

Access: permission billing.manage in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
messaging_plan essential, team, enterprise, or null for none (required).
voice The phone system (required).

Response: As GET /billing/subscription.

GET /api/v1/billing/subscription/estimate

The monthly statement the organization would have with these plans (its seats of today).

The peak of lines of the current period beyond the included ones (in arrears, extra_line) is added when the organization already has the phone system. Errors are {error, message}: 400 no_organization, 400 validation_error with field messaging_plan (unknown plan, or a plan the domain no longer offers).

Access: permission billing.view or billing.manage in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Query parameters

Name Description
messaging_plan essential, team or enterprise (an active plan of the domain); none: without a messaging plan; absent or empty: the plan the organization holds (or the one pending for the next anniversary; none without a subscription).
voice 1 or true for the phone system; anything else (or absent): without it.

Response: lines: [{code, description, detail, quantity, unit_price, amount}] (detail says what is counted and what is included). total: Total of the month. seats: What is counted {users, full_users, virtual_users, recording_extensions, numbers, tollfree_numbers, teams, widgets}.

GET /api/v1/billing/transactions

The operations of the balance (recharges, usage, monthly fees, refunds, gifts, adjustments), newest first.

Not available to restricted API keys (a key limited to a team, or an integration's key): 403 with error forbidden.

Errors are {error, message}: 400 validation_error with field (skip or limit not an integer; from or to not an ISO 8601 date; category, type or number_id invalid).

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Query parameters

Name Description
from ISO 8601 date or datetime, lower bound (UTC).
to ISO 8601 date (the whole day) or datetime, upper bound.
category all, communications, payments, recurring or admin.
type all, purchase, usage, refund, adjustment or bonus.
number_id Only the operations of this phone number (calls, monthly fees).
skip Offset (default 0).
limit Page size (default 50, brought back into 1-100).

Response: transactions: The page: {id, organization_id, type, category, subtype (machine name: recharge_manual, recharge_auto, sms_out, sms_in, mms_out, mms_in, call, did_monthly, voicemail_monthly, recording_monthly, sms_monthly, flow_monthly, refund, chargeback, gift, admin_credit, admin_fix...), amount, balance_after, currency, description, phone_number_id, call_id, invoice_id, payment_method {brand, last4}, refund_id, reference_id, reference_type, created_at}. total: Number of transactions matching the filters, whatever the page. skip: Offset applied. limit: Page size applied.

GET /api/v1/billing/usage

Usage records (one per charged message), newest first.

Not available to restricted API keys (a key limited to a team, or an integration's key): 403 with error forbidden.

Errors are {error, message}: 400 validation_error with field for a skip or limit that is not an integer or a date that is not ISO 8601.

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Query parameters

Name Description
start_date ISO 8601 date or datetime, lower bound (a value without time zone is UTC).
end_date ISO 8601 date or datetime, upper bound.
type Usage type (sms, mms...).
skip Offset (default 0).
limit Page size (default 50, brought back into 1-100).

Response: usage_records: The page of records. total: Number of records matching the filters, whatever the page. skip: Offset applied. limit: Page size applied.

GET /api/v1/billing/usage/detailed

Detailed usage of the billing period (from the billing anniversary) with totals.

Not available to restricted API keys (a key limited to a team, or an integration's key): 403 with error forbidden.

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Query parameters

Name Description
start_date ISO 8601 date or datetime (default: start of the billing period). 400 validation_error when it is not a date.
end_date ISO 8601 date or datetime (default: now, never later).

Response: detailed: Usage by type with unit prices, discounts and costs. totals: {total_cost}. period: {start, end} as dates.

GET /api/v1/billing/usage/summary

Usage totals by type for a period (the current month by default).

Not available to restricted API keys (a key limited to a team, or an integration's key): 403 with error forbidden.

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Query parameters

Name Description
start_date ISO 8601 date or datetime (default: first of the month). 400 validation_error when it is not a date.
end_date ISO 8601 date or datetime (default: now).

Response: summary: {type: {count, total_cost}}.

GET /api/v1/organizations/<org_id>/billing

Billing overview of any organization (super admin only); same response as GET /billing.

Access: platform permission platform_admin
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
org_id Organization ID.

Response: billing: Billing record. pricing_plan: Pricing plan in force. current_month_usage: Usage of the billing period. billing_period: start and end of the period. recent_transactions: The latest credit transactions.

POST /api/v1/organizations/<org_id>/billing/backfill

Backfill usage records for unbilled messages and DIDs (super admin only).

Access: platform permission platform_admin
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
org_id Organization ID.

Response: messages_processed: Messages found without usage record. usage_records_created: Usage records created. by_type: Usage records created by type (sms_outbound, sms_inbound, mms_outbound, mms_inbound). total_credits_deducted: Credit debited for those messages. dids_charged, dids_cost: Numbers charged for the month and their cost. subscription_charged, subscription_cost: Whether the monthly subscription was charged, and its cost.

This will 1. Find all messages without usage records and create them 2. Charge monthly DID fees if not already charged this month

GET /api/v1/organizations/<org_id>/billing/backfill/preview

Preview what would be backfilled for an organization (super admin only); nothing is written.

Access: platform permission platform_admin
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
org_id Organization ID.

Response: messages: Messages without usage record, by type (sms_outbound, sms_inbound, mms_outbound, mms_inbound): {count, segments, cost}. dids: Numbers not billed this month: {standard, tollfree}, each {count, cost}. subscription: {due, cost} the monthly subscription of the plan, when due. totals: {messages_count, messages_cost, dids_count, dids_cost, subscription_cost, total_cost}.

POST /api/v1/organizations/<org_id>/billing/charge-dids

Charge monthly DID fees for a specific organization (super admin only).

Only DIDs not yet billed this month are charged; when the domain charges its monthly fees itself (settings.billing.monthly_fees, E40), its run for this organization is made instead.

Access: platform permission platform_admin
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
org_id Organization ID.

Response: charged_count: Number of numbers charged. total_cost: Amount debited. charged_dids: The numbers charged, with their price.

POST /api/v1/organizations/<org_id>/billing/reconcile

Reconcile usage records with credit transactions (super admin only).

Access: platform permission platform_admin
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
org_id Organization ID.

Response: usage_records_found: Usage records of messages examined. already_credited: Those that already had their credit transaction. missing_transactions: Those without one. reconciled_count: Credit transactions created. total_deducted: Credit debited. failed_insufficient_balance: Records left without a transaction for want of credit.

This will 1. Find usage records that don't have corresponding credit transactions 2. Create the missing credit transactions (deduct credits) Use this to fix cases where usage records were created without proper credit deductions.

GET /api/v1/organizations/<org_id>/billing/usage/detailed

Detailed usage of any organization (super admin only); same response as GET /billing/usage/detailed.

Access: platform permission platform_admin
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
org_id Organization ID.

Query parameters

Name Description
start_date ISO 8601 date or datetime (default: start of the billing period).
end_date ISO 8601 date or datetime (default: now).

Response: detailed: Usage by type. totals: {total_cost}. period: {start, end}.

Free trial, account state and settings

Free trial, state of the organization's account and the account settings of a domain (sign-up, trial, deletion, notifications).

GET /api/v1/billing/trial

The free trial of the organization: whether it can be started, and where it stands.

The trial is off until the domain turns it on (settings.trial.enabled), and an organization has one in its life. A trial whose balance reached minimum_credit is converted to a normal account when this is read. In the administration context nothing is eligible (the trial is started by the customer).

Access: member of the organization
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: trial: eligible (can be started now), active, started_at and expires_at (UTC, null before the start), minimum_credit (the credit that turns the trial into a normal account), duration_days (of a new trial), state (active, ended, converted or null), reason (why it is not eligible, null when it is: trial_disabled, already_used, funded, has_numbers, phone_required, phone_not_verified, phone_country, phone_already_used, admin_context).

POST /api/v1/billing/trial/activate

Start the free trial: a number of the domain pool is given to the organization.

The member who asks must have a verified phone (E04): one phone number opens one trial per domain. The number goes to the organization's default team. Refused in the administration context (403 forbidden).

Access: permission billing.manage in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
country CA or US (required; 400 country_unsupported for another one).
type local or tollfree (optional).
area_code Three digits (optional).
city Part of the city name (optional; only for numbers the pool describes with a city).

Response: 201 with phone_number (the number given) and trial (as GET /billing/trial, now active).

Errors 403 trial_restricted with reason (see GET /billing/trial), 409 number_unavailable (no free number fits), 400 validation_error.

GET /api/v1/domains/<domain_id>/account-settings

The account settings of a domain (domain admin of the domain, or super admin).

Everything here is off or neutral by default, so Textos and VoXoS domains are untouched: free sign-up (closed), free trial (off), welcome e-mail (off), the notification defaults (see GET /auth/me/notifications), the grace period of an account deletion (10 days), the default time zone of new accounts (none) and the longest SMS body (800 characters).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
domain_id Domain ID.

Response: account_settings: signup ({enabled, initial_credit, max_per_ip_per_day, blocked_email_domains, captcha: {provider, site_key, secret (masked), min_score}, terms_url}), trial ({enabled, duration_days, minimum_credit, initial_credit, require_verified_phone, phone_countries, cleanup_enabled, cleanup_months}), account_deletion ({grace_days}), notifications ({welcome_email, defaults}), profile ({default_timezone}), sms ({max_body_length}).

PUT /api/v1/domains/<domain_id>/account-settings

Change the account settings of a domain: the sections and fields given are changed, the others kept.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
signup Any of enabled (boolean), initial_credit (0 to 1000), max_per_ip_per_day (1 to 1000), blocked_email_domains (list of domain names), captcha ({provider: none, recaptcha or turnstile, site_key, secret, min_score}; the secret sent as ** keeps the stored one), terms_url (https URL or null).
trial Any of enabled, duration_days (1 to 90), minimum_credit, initial_credit (0 to 1000), require_verified_phone, phone_countries (list of ISO codes or null), cleanup_enabled, cleanup_months (1 to 36).
account_deletion {grace_days: 0 to 90}.
notifications {welcome_email: boolean, defaults: {email: {...}, push: {...}} as GET /auth/me/notifications}.
profile {default_timezone: IANA name or null}.
sms {max_body_length: 0 (no limit) to 5000}. An unknown section or field, or a wrong value, gets 400 validation_error with field.

Response: account_settings: All the settings after the change, as GET.

POST /api/v1/domains/<domain_id>/trial/run

Run now the round that converts, reminds and ends the free trials of the domain (the server does the same every ten minutes; idempotent).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
dry_run Count what is due, write and send nothing (boolean, default false).

Response: result: {examined, converted, reminders, ended, cleaned}.

GET /api/v1/organizations/current/status

The state of the account in one call: active, suspended (negative balance, E46) or trial.

Access: member of the organization
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: status: state (suspended while the balance cycle blocks the account, trial during the free trial, else active), balance, currency, low_balance (the balance is low or empty, see balance_update), trial ({active, expires_at}), capabilities (the capabilities of the organization, or of its domain when it has none of its own).

Billing: cards and receipts

Billing address, cards (verified with the payment provider), recharge quote, low-balance alert, history export, PDF receipts, number reactivation.

GET /api/v1/billing/info

Billing address of the organization (the empty shape when none was saved yet).

Not available to restricted API keys: 403 forbidden. Errors are {error, message}: 400 no_organization.

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: billing_info: {usage (personal or business), company_name, first_name, last_name, address_1, address_2, city, state, postal_code, country, confirm_not_canadian, tax_region (the province, US, INTL, or null when the address is not usable)}.

PUT /api/v1/billing/info

Save the billing address. It decides the taxes of the recharges (tax table of the domain).

Errors are {error, message}: 400 validation_error with field (usage, company_name, first_name, last_name, address_1, city, country, state, postal_code, confirm_not_canadian).

Access: permission billing.manage in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
usage personal or business.
company_name Company name (required for business).
first_name First name.
last_name Last name.
address_1 Street address.
address_2 Second line (optional).
city City.
state Province (Canada: AB, BC, MB, NB, NL, NS, NT, NU, ON, PE, QC, SK, YT) or US state.
postal_code Canadian postal code (A1A 1A1) or US ZIP.
country ISO 3166-1 alpha-2 code.
confirm_not_canadian Must be true for an address outside Canada.

Response: billing_info: The saved address with its tax_region.

GET /api/v1/billing/invoices/<invoice_id>/pdf

The receipt of an invoice as a PDF (seller, buyer, lines, taxes with the seller's registration numbers, card), in the language the receipt was issued in.

Not available to restricted API keys: 403 forbidden. Errors are {error, message}: 404 not_found, 403 forbidden (invoice of another organization).

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
invoice_id Invoice ID.

Response: application/pdf.

PUT /api/v1/billing/low-balance

Set the low-balance alert: by e-mail, push and the real-time event low_balance when the balance falls to the threshold, at most every 7 days.

Errors are {error, message}: 400 validation_error with field (enabled not a boolean, threshold not a number or not among the domain's choices).

Access: permission billing.manage in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
enabled Whether the alert is on.
threshold Balance (CAD) at or under which the alert is sent, 0 or more.

Response: low_balance: {enabled, threshold}.

POST /api/v1/billing/numbers/<phone_id>/reactivate

Bring back a number that was switched off because its monthly fees could not be paid: debits what the number itself owes for the current month and lifts the pause. The functions that were turned off (voicemail, recording, SMS) stay off; turn them back on afterwards.

Errors are {error, message}: 404 not_found (not a number of this organization), 409 conflict (the number was not paused for unpaid fees), 402 insufficient_balance (add credit first; with owed and balance).

Access: permission billing.manage in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
phone_id Phone number ID.

Response: reactivated: true. owed: Amount debited. balance: Balance after.

GET /api/v1/billing/payment-methods

The cards of the organization, oldest first.

Not available to restricted API keys: 403 forbidden. Errors are {error, message}: 400 no_organization.

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Response: payment_methods: [{id, provider, brand, last4, exp_month, exp_year, card_type, cardholder_name, is_default, is_expired, used_by_auto_recharge}]. No token is ever returned.

POST /api/v1/billing/payment-methods

Add a card from the token the SDK returned. The server asks the provider what the token is and stores brand, last four digits, expiry and fingerprint from the provider's answer, never from the client. The first card becomes the main one.

Errors are {error, message}: 400 card_invalid / card_expired (with field), 409 duplicate_payment_method, 422 card_unsupported (only Visa and Mastercard) / too_many_payment_methods, 502 provider_error.

Access: permission billing.manage in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden
Rate limit: 30 per hour

Request body

Name Description
provider The provider (default: the domain's).
user_payment_option_id The card token returned by the SDK (required).
user_token_id The user_token_id of the session (must be the organization's).
cardholder_name Name on the card.
initial_transaction_id The provider's id of the verification transaction, used by the automatic recharge.
make_default Make it the main card.

Response: payment_method: The stored card (201).

DELETE /api/v1/billing/payment-methods/<payment_method_id>

Delete a card. If it was the main card the oldest remaining card takes over; the automatic recharge follows it, and goes off with the last card.

Errors are {error, message}: 404 not_found.

Access: permission billing.manage in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
payment_method_id Card ID.

Response: success: true. payment_methods: The remaining cards.

POST /api/v1/billing/payment-methods/<payment_method_id>/default

Make a card the main one.

Errors are {error, message}: 404 not_found.

Access: permission billing.manage in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Path parameters

Name Description
payment_method_id Card ID.

Response: payment_methods: The cards of the organization.

POST /api/v1/billing/payment-methods/session

Open a session of the payment provider for the browser SDK. The card is collected by the SDK on the provider's side and never reaches this server; the SDK gives back a token that POST /billing/payment-methods then verifies.

Errors are {error, message}: 400 configuration_error (no payment provider), 502 provider_error / provider_unavailable.

Access: permission billing.manage in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden
Rate limit: 30 per hour

Response: session: {provider, session_token, client_request_id, user_token_id (give it to the SDK as userTokenId), merchant_id, merchant_site_id, environment, expires_at}.

POST /api/v1/billing/payment-methods/tokenize

Turn a TEST card into a token. Only with the fake payment provider of development and test domains; any real provider answers 404 tokenize_unavailable: a real card number never goes through this server.

Errors are {error, message}: 400 card_invalid / card_expired (with field), 422 card_unsupported, 404 tokenize_unavailable.

Access: permission billing.manage in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden
Rate limit: 30 per hour

Request body

Name Description
card_number A test card number (4242 4242 4242 4242 works; 4000 0000 0000 3220 asks for a verification, code 123456).
exp_month Expiry month.
exp_year Expiry year (four digits).
cvv Security code (not kept).
cardholder_name Name on the card.

Response: user_payment_option_id, user_token_id, brand, last4; or verification {id, type, expires_at} when the card needs a verification.

POST /api/v1/billing/payment-methods/tokenize/verify

Complete the verification of a TEST card (fake provider only), three attempts.

Errors are {error, message}: 404 not_found (unknown or expired), 400 invalid_code, 400 verification_failed (third failure).

Access: permission billing.manage in the organization (profiles)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden
Rate limit: 30 per hour

Request body

Name Description
verification_id The id returned by tokenize.
code The code (123456 for the test provider).

Response: user_payment_option_id, user_token_id, brand, last4.

POST /api/v1/billing/recharge/quote

What a recharge would cost, computed by the server: credits, promotion, subtotal, taxes line by line (tax table of the domain, billing address of the organization) and total. Nothing is charged.

Errors are {error, message}: 400 validation_error (amount: not a number, more than two decimals, under the minimum), 409 billing_info_required (the domain charges taxes and the address is missing or unusable), 422 max_balance (the balance would pass the cap; with maximum).

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Request body

Name Description
amount Credits to buy.
promo_code A promotional code (optional). A refused code does not fail the quote: promo_info.error says why.

Response: quote: {credits, promotion, subtotal, taxes [{code, label, rate, amount}], total, currency, minimum, maximum (null: no cap), credited (credits plus a promotional gift), promo_info ({code, operation, error, min_credit} or null), expires_at}.

GET /api/v1/billing/transactions/export

The transaction history as a CSV file (UTF-8 with a byte-order mark, opens in a spreadsheet), same filters as GET /billing/transactions, newest first, at most 20000 rows.

Errors are {error, message}: 400 validation_error with field (from, to, category, type, number_id, format), 413 too_many_rows.

Access: authenticated (JWT or API key)
API keys: refused to restricted keys (limited to a team, or an integration's), 403 forbidden

Query parameters

Name Description
from ISO 8601 date or datetime, lower bound (UTC).
to ISO 8601 date (the whole day) or datetime, upper bound.
category all, communications, payments, recurring or admin.
type all, purchase, usage, refund, adjustment or bonus.
number_id Only the operations of this phone number.
format csv (the only one).

Response: A text/csv attachment: date, type, category, subtype, description, amount, balance_after, currency, number, call_id, invoice_number, card.

Domain billing

Billing settings and tax table, payment provider, promotional codes, payments and refunds, monthly fees of the numbers (domain administrators).

GET /api/v1/domains/<domain_id>/billing-overview

What the organizations of the domain are billed: one row each (plan, last statement, next statement estimated, debits of the month, communications, last month, balance) and the totals.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Response: month: The current calendar month {start, end}. organizations: [{id, name, plan, plan_name (in the language of the request), voice, lines (phone system: {included, limit, peak, at, overage, overage_cost, manual, max_channels}, else null), balance, last_statement null or {total, date}, next {date (null for usage_only), total, kind subscription|monthly_fees|usage_only}, this_month (debits since the 1st), communications (usage debited since the 1st), previous_month (debits of the last calendar month)}], sorted by name. An organization counts as subscribed only while the domain offers subscriptions. truncated (boolean): Only the first 2000 organizations are listed. totals: {this_month, communications, previous_month, next, balance}.

GET /api/v1/domains/<domain_id>/billing-settings

What the domain decides about prepaid billing (settings.billing), defaults filled in. A domain that never set them has no taxes, no balance cap and the recharge it always had.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Response: billing_settings: {currency (CAD), min_recharge, max_balance (null: no cap), tax_rates ({region: [{code, label, rate, number}]} or null for no taxes; regions are the Canadian province codes, US and INTL), seller ({name, address_1, address_2, city, state, postal_code, country, email, phone} printed on receipts), invoice_prefix, billing_path (page the e-mails link to), quote_ttl_minutes, receipt_email, auto_recharge_retry_hours, threshold_choices (null: any), low_balance_repeat_days, low_balance_alerts (alert on for organizations that never chose), monthly_fees ({enabled, day, hour_utc, forecast_enabled, forecast_day})}. tax_presets: Names of the tax tables tax_preset can fill in (canada).

PUT /api/v1/domains/<domain_id>/billing-settings

Change the billing settings of the domain (the fields given are changed, the others kept). Journaled.

Errors are {error, message}: 400 validation_error with field.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
min_recharge Smallest recharge and smallest automatic recharge, CAD (default 1).
max_balance Cap of the balance after a recharge, or null for none.
tax_preset canada: fill the tax table with the federal and provincial rates (TPS, TVQ, TVH); tax_numbers {tps, tvq, tvh} adds the seller's registration numbers.
tax_numbers Registration numbers by tax code, used with tax_preset.
tax_rates The whole tax table {region: [{code, label, rate, number}]}, or null / {} for no taxes. A line is computed on the subtotal and rounded to the cent.
seller Name and address printed on receipts.
invoice_prefix Receipts are numbered <prefix>-<year>-<sequence> (2 to 8 capital letters or digits); give each seller its own.
billing_path Path of the page the e-mails send customers to (default /settings/billing).
quote_ttl_minutes How long a quote is shown as valid (1 to 120).
receipt_email Send a receipt by e-mail after a recharge.
auto_recharge_retry_hours Hours without a new automatic attempt after a failure (0 to 720).
threshold_choices Allowed thresholds of the automatic recharge and the low-balance alert (a list, or null for any).
low_balance_repeat_days Least days between two low-balance alerts (0 to 365).
low_balance_alerts Turn the low-balance alert (threshold 10) on for organizations that never chose.
monthly_fees {enabled, day (1-28), hour_utc, forecast_enabled, forecast_day}: the monthly fees of the numbers (E40), off by default.

Response: billing_settings: The settings after the change (as GET).

GET /api/v1/domains/<domain_id>/monthly-fees

The monthly fees charged to the organizations of the domain for a period, and the numbers switched off for unpaid fees. The fees are turned on with monthly_fees.enabled in the billing settings.

Errors are {error, message}: 400 validation_error with field (period).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Query parameters

Name Description
period YYYY-MM (default the current month).
organization_id Only this organization (an organization that is not one of the domain matches nothing: empty lists, never another domain's data).
skip Offset (default 0).
limit Page size (default 50, 1-100).

Response: period: The period. charges: [{organization_id, number_id, feature (did, voicemail, recording, sms or flow), amount, source (run or acquisition), charged_at}]. total: Number of charges. switched_off: [{organization_id, number_id, features, fees, balance, at}] for the period. skip: Offset applied. limit: Page size applied.

POST /api/v1/domains/<domain_id>/monthly-fees/run

Run the monthly fees of the domain now (the server runs them every hour from the configured day and hour; idempotent: what was charged for the period is never charged twice). For each organization, number by number, the oldest first: the fees of the number and of its functions are debited together; if the balance falls short the automatic recharge is tried once; if it still does, the number is switched off (functions off, number paused) and the organization is told by e-mail and SMS. Journaled.

Errors are {error, message}: 409 conflict (monthly fees are not turned on for the domain).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
organization_id Only this organization.
dry_run Compute and show what would be charged, write and send nothing.
force Run even before the configured day and hour of the month.

Response: domains, organizations: What was looked at. charged: Fees debited. total: Their amount. recharged: Catch-up recharges made. switched_off: Numbers switched off. forecasts: Warnings of the 25th sent. errors: Organizations that failed (see the logs).

GET /api/v1/domains/<domain_id>/organizations/<organization_id>/subscription

The subscription of an organization of the domain (as GET /billing/subscription).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.
organization_id Organization ID.

Response: As GET /billing/subscription.

PUT /api/v1/domains/<domain_id>/organizations/<organization_id>/subscription

Choose the plans of an organization of the domain (same rules as PUT /billing/subscription, without requiring its automatic recharge). Journaled.

Errors are {error, message}: 404 not_found (organization not in the domain); 400 validation_error with field (field missing, unknown or inactive plan, voice not a boolean, subscriptions not offered by the domain, a first subscription with neither a plan nor the phone system); 402 plan_limit {limit, plan, upgrade, current}: the organization already has more users or teams than the chosen plan allows. The domain administrator cannot force it either (as for an invitation): remove members or teams first, or raise max_users / max_teams of the plan (PUT /domains/<id>/subscription-settings).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.
organization_id Organization ID.

Request body

Name Description
messaging_plan essential, team, enterprise, or null for none (required).
voice The phone system (required).

Response: As GET /billing/subscription.

GET /api/v1/domains/<domain_id>/payment-setup

Where the payment provider stands for this domain: what is configured, the URL to give the processor for its notifications (DMN), and a checklist of what is still missing before going live. No secret is returned.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Response: payment_setup: {provider (the one the domain pays with, or null), enabled_providers, nuvei {enabled, environment, merchant_id, merchant_site_id, has_secret_key, notification_url}, fake_allowed, checks [{key, ok, detail?}], ready (every check but nuvei_production, tax_numbers and tax_table is ok)}.

POST /api/v1/domains/<domain_id>/payment-setup/test

Check the credentials of the domain's payment provider by opening a session with the processor (Nuvei getSessionToken). Calls the processor for real: use it after entering the sandbox credentials. Nothing is charged or stored.

Errors are {error, message}: 400 configuration_error (no provider enabled or credentials missing), 502 provider_error / provider_unavailable (the processor refused: bad merchant id, site id or secret key, or unreachable).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Response: ok: true. provider: The provider tested. environment: sandbox or production.

GET /api/v1/domains/<domain_id>/payments

Card payments of the organizations of the domain, newest first (the card debits, apart from the credits and receipts they produced).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Query parameters

Name Description
organization_id Only this organization.
status pending, approved, completing, completed, declined, failed, requires_action or unknown.
skip Offset (default 0).
limit Page size (default 50, 1-100).

Response: payments: [{id, organization_id, kind (manual or auto), provider, status, card {brand, last4}, credits, promotion, credited, promo_code, subtotal, taxes, total, currency, provider_transaction_id, decline_code, error, invoice_id, transaction_id, refunded_total, created_at, completed_at}]. total: Number of payments matching. skip: Offset applied. limit: Page size applied.

POST /api/v1/domains/<domain_id>/payments/<payment_id>/refund

Refund a completed payment to the customer's card and take the credits back. The credits come off proportionally to the money refunded (all of them, gift included, for a full refund); the balance may go negative, which hands the account to the negative-balance lifecycle. A full refund cancels the receipt. Journaled.

Errors are {error, message}: 404 not_found, 409 conflict (not completed, already refunded, another refund in progress), 400 validation_error (amount), 502 refund_failed (the provider refused: nothing was taken back).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.
payment_id Payment ID.

Request body

Name Description
amount Money to give back (default: all that is left to refund).
reason Note for the audit log.

Response: refund: {id, payment_id, amount, credits (taken off the balance), reason, status, provider_refund_id, created_at}. payment: The payment after the refund. transaction: The ledger row of the debit.

POST /api/v1/domains/<domain_id>/payments/<payment_id>/resolve

Settle a payment whose outcome is unknown (the processor did not answer) after checking it in the processor's console: approved credits the customer, failed closes it without crediting. Journaled.

Errors are {error, message}: 404 not_found, 409 conflict (the payment is not unknown), 400 validation_error with field.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.
payment_id Payment ID.

Request body

Name Description
outcome approved or failed.
provider_transaction_id The processor's transaction id (required for approved).

Response: payment: The payment after it was settled.

POST /api/v1/domains/<domain_id>/payments/reconcile

Finish the payments a crash left half done (approved by the processor but not credited yet) and flag the ones stuck pending. The server does the same every few minutes; this runs it now.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Response: completed: Payments finished now. marked_unknown: Pending payments flagged unknown. errors: Payments that could not be finished (see the logs).

GET /api/v1/domains/<domain_id>/promo-codes

Promotional codes of the domain, newest first.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Query parameters

Name Description
skip Offset (default 0).
limit Page size (default 50, 1-100).

Response: promo_codes: [{id, code, operation (gift or discount), value, min_credit, valid_from, valid_to, per_account_limit, max_uses, uses, active, description, created_at}]. total: Number of codes. skip: Offset applied. limit: Page size applied.

POST /api/v1/domains/<domain_id>/promo-codes

Create a promotional code. A gift adds value credits (a separate gift ledger row) on top of the credits bought; a discount lowers the price of the credits by value (taxes are computed after it).

Errors are {error, message}: 400 validation_error with field, 409 conflict (the code exists).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
code The code, 3 to 32 letters, digits, - or _ (case does not matter).
operation gift or discount.
value Amount in CAD.
min_credit Smallest recharge it applies to (default 0).
valid_from ISO 8601 start of validity (optional).
valid_to ISO 8601 end of validity (optional).
per_account_limit Times one organization can use it (default 1).
max_uses Times it can be used in all (default unlimited).
active Whether it can be used (default true).
description Note for the administrators.

Response: promo_code: The created code (201).

DELETE /api/v1/domains/<domain_id>/promo-codes/<promo_id>

Delete a promotional code that was never redeemed; a redeemed one is kept for the history (deactivate it with active: false).

Errors are {error, message}: 404 not_found, 409 conflict (already redeemed).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.
promo_id Code ID.

Response: success: true.

PUT /api/v1/domains/<domain_id>/promo-codes/<promo_id>

Change a promotional code (the fields given). The code itself and its operation cannot change once redeemed.

Errors are {error, message}: 404 not_found, 400 validation_error with field, 409 conflict.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.
promo_id Code ID.

Request body

Name Description
value Amount in CAD.
min_credit Smallest recharge it applies to.
valid_from ISO 8601 start of validity, or null.
valid_to ISO 8601 end of validity, or null.
per_account_limit Times one organization can use it.
max_uses Times it can be used in all, or null.
active Whether it can be used.
description Note for the administrators.
code New code (only while never redeemed).
operation gift or discount (only while never redeemed).

Response: promo_code: The code after the change.

GET /api/v1/domains/<domain_id>/subscription-settings

The subscriptions the domain offers: on or off, messaging plans, phone system, extras and price of the messages beyond those included.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Response: subscriptions: {enabled, plans [{key, name {fr, en}, price, included {messages, users, teams, numbers, widgets}, max_users, max_teams, active}], overage_tiers [{up_to, price}], mms_weight, voice {price, included_numbers, full_user, virtual_user, minutes_per_full_user, minute_overage, warn_ratio, tollfree_inbound_per_minute, recording_per_extension, included_lines, lines_per_full_user, burst_lines, extra_line}, addons {number, tollfree_number, team, widget}}.

PUT /api/v1/domains/<domain_id>/subscription-settings

Change the subscriptions the domain offers (the fields given are changed). Journaled.

Errors are {error, message}: 400 validation_error with field.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
enabled Offer the subscriptions to the organizations of the domain.
plans [{key (essential, team, enterprise), price, active, max_users, max_teams, included {messages, users, teams, numbers, widgets}, name {fr, en}}] (the fields given of each plan; max_users, max_teams and included.users accept null = no limit; a name is cut at 40 characters).
overage_tiers The whole list [{up_to, price}] (1 to 20 tiers), increasing up_to, the last one null.
mms_weight Messages an MMS counts for (1 to 10).
voice {price, included_numbers, full_user, virtual_user, minutes_per_full_user, minute_overage, warn_ratio (0.1 to 1), tollfree_inbound_per_minute, recording_per_extension, included_lines, lines_per_full_user, burst_lines (null: as many as the included lines, at least 2), extra_line} (the fields given).
addons {number, tollfree_number, team, widget} (the fields given). An unknown field (at any level) is a 400 validation_error naming it.

Response: subscriptions: The settings after the change.

Payment notifications

URL called by the payment provider (Nuvei DMN) to confirm a payment, a refund or a chargeback.

GET /webhook/billing/<provider>/<secret>

Receive a notification of the payment processor (public: the secret of the URL and the signature authenticate it).

Errors are {error, message}: 404 not_found (unknown provider or secret), 401 invalid_signature, 400 provider_unsupported.

Access: public (no authentication)
Rate limit: exempt from the general per-address limit

Path parameters

Name Description
provider nuvei.
secret The webhook_secret of the provider's settings in the domain.

Response: The text OK (the processor retries until it gets a 200).

Errors 500 internal_error: the notification could not be processed (the processor retries).

POST /webhook/billing/<provider>/<secret>

Receive a notification of the payment processor (public: the secret of the URL and the signature authenticate it).

Errors are {error, message}: 404 not_found (unknown provider or secret), 401 invalid_signature, 400 provider_unsupported.

Access: public (no authentication)
Rate limit: exempt from the general per-address limit

Path parameters

Name Description
provider nuvei.
secret The webhook_secret of the provider's settings in the domain.

Response: The text OK (the processor retries until it gets a 200).

Errors 500 internal_error: the notification could not be processed (the processor retries).

Domains

Whitelabel domains: settings, numbers, providers, domain admins, voice.

GET /api/v1/domains

List all domains (platform_admin), or only the domains a domain_admin administers.

Access: platform permission platform_admin or domain_admin

Response: domains: The domains.

POST /api/v1/domains

Create a new domain.

Access: platform permission platform_admin

Request body

Name Description
name Domain identifier (slug), a string of 1 to 120 characters (400 validation_error otherwise).
hostnames List of hostnames to match (can include port).
display_name Display name for the domain, a string of 1 to 120 characters.

Response: domain: The created domain. message: Confirmation.

DELETE /api/v1/domains/<domain_id>

Delete a domain.

Access: platform permission platform_admin

Path parameters

Name Description
domain_id Domain ID.

Response: Success message.

GET /api/v1/domains/<domain_id>

Get a domain by ID.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: Domain details.

PUT /api/v1/domains/<domain_id>

Update a domain.

A body with name or hostnames is refused to any request authenticated by an API key, ordinary or restricted: 403 (error forbidden, message "API key not allowed on this route"). The hostnames give the address of the links of the domain's e-mails (password reset, invitations), so only a session (JWT) changes them. The body must be a JSON object and its fields of the documented types (else 400 validation_error).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused when the body contains hostnames, name

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
name Domain identifier (slug), a string of 1 to 120 characters.
hostnames List of hostnames to match (strings; a single string is accepted).
display_name Display name for the domain, a string of 1 to 120 characters.
capabilities List of capabilities of the domain (adding sip, call_codes or voicemail_boxes is reserved to a platform_admin: 403 forbidden for a domain_admin): textos, voxos, sip (SIP endpoints and softphone provisioning), call_codes (call codes, legacy without interface: no app uses them any more), voicemail_boxes (voicemail boxes of the endpoints and independent boxes); sip, call_codes and voicemail_boxes need voxos in the same list, else 400 validation_error (field capabilities). An organization of the domain holds a subset of this list (PUT /organizations/<id>/capabilities).

Response: Updated domain.

GET /api/v1/domains/<domain_id>/admins

List all domain admins and pending invitations for a domain.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: admins: The domain admin users. pending_invitations: The invitations to become domain admin that are not accepted yet.

POST /api/v1/domains/<domain_id>/admins

Add a domain admin to a domain.

If the user exists, add them immediately. If the user doesn't exist, create an invitation.

Answers 201 in both cases. 400 validation_error: email missing, or the account is a super admin. 409 conflict: already a domain admin of this domain. 403 forbidden: the account belongs to organizations the caller does not administer (only a super admin turns such an account into a domain admin).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
email Email of the user to add as domain admin.

Response: For an existing account: admin (the user, now domain admin), message and invited false. For an e-mail with no account: invitation (with its token), message, invited true and email_sent (whether the e-mail went out).

DELETE /api/v1/domains/<domain_id>/admins/<user_id>

Remove a domain admin from a domain.

The domain leaves the account's domain_ids; without any domain left, the account loses the platform permission domain_admin and its role becomes the one its profiles give in its current organization (agent without membership). 400 validation_error when the account is not a domain_admin.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.
user_id User ID to remove.

Response: message: Confirmation. new_role: domain_admin when the user still administers other domains, else the role held in the current organization (agent without membership).

DELETE /api/v1/domains/<domain_id>/admins/invitations/<invitation_id>

Delete a pending domain admin invitation.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
invitation_id Invitation ID to delete.

Response: Success message.

GET /api/v1/domains/<domain_id>/negative-balance

Settings of the negative-balance lifecycle of a domain (E46).

Voice calls are debited after they took place, so a balance can become negative. The organization then goes through a grace period (notices by e-mail and SMS), is suspended (sending and calls refused, numbers paused), held in limbo (numbers kept in reserve) and released (numbers back in the pool of the domain). It runs only while voice billing is on for the domain (GET /domains/<id>/voice-billing) and enabled is not false.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: negative_balance: {enabled, grace_days, suspended_days, limbo_days, notice_days (null: the default steps), effective_notice_days, channels ({email, sms}), recharge_guard_minutes}. active: Whether the lifecycle runs for the domain now.

PUT /api/v1/domains/<domain_id>/negative-balance

Change the settings of the negative-balance lifecycle of a domain (the fields given are changed).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
enabled Run the lifecycle (boolean, default true; it also needs voice billing on).
grace_days Days between the balance becoming negative and the suspension, 0 to 365 (default 10).
suspended_days Days the account stays suspended (numbers paused, kept) before the limbo, 0 to 365 (default 0).
limbo_days Days the numbers are held in reserve before they go back to the pool, 0 to 730 (default 35).
notice_days Days after the balance became negative at which a notice goes out during the grace period: null for the default steps (at once, half way, 2 days and 1 day before the end), or at most 4 days below grace_days.
channels {email, sms} booleans (both true by default); the SMS only goes to admins with a verified phone.
recharge_guard_minutes Minutes after an automatic recharge (running, succeeded or failed) during which the organization is not moved forward, 0 to 1440 (default 30).

Response: negative_balance: The settings after the change (as GET). active: Whether the lifecycle runs for the domain now.

GET /api/v1/domains/<domain_id>/negative-balance/organizations

Organizations of the domain whose balance is negative or in the middle of the lifecycle.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Query parameters

Name Description
state Only this state: negative_grace, suspended, limbo or released (optional).
skip Pagination offset (default 0).
limit Page size (default 50, brought back into 1-100).

Response: organizations: The page of {organization_id, name, credit_balance, balance_state, negative_since, grace_ends_at, suspended_at, limbo_at, release_at, released_at, numbers ({paused, held, released})}; dates are UTC (ISO 8601 with Z). total: Number of organizations matching. skip: Offset applied. limit: Page size applied.

POST /api/v1/domains/<domain_id>/negative-balance/run

Advance now the lifecycle of the domain's organizations (the periodic task does the same).

Idempotent: a notice already sent is never sent again.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
organization_id Only this organization (optional).
dry_run Compute the transitions and the notice due, write and send nothing (boolean, default false).

Response: result: {examined, transitions, notices, retried, errors, by_state}; with organization_id, {organization_id, from, to, transitions, notice, retried}.

GET /api/v1/domains/<domain_id>/organizations

List organizations in a domain.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: organizations: All the organizations of the domain. total: Number of organizations.

POST /api/v1/domains/<domain_id>/organizations/<organization_id>/credit

Add credit to, or take credit back from, an organization of the domain by hand (administrator of the domain or super admin).

A negative amount is a correction (admin_fix in the ledger): it needs a reason and may bring the balance below zero, which starts the negative-balance lifecycle like any debt. The credit is written in the ledger as an administrative operation (admin_credit, with the administrator and the reason in the audit journal). A negative balance brought back to zero or above leaves the negative-balance lifecycle like after a recharge; messages held for lack of credit are sent. Not available to API keys.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.
organization_id Organization ID (must belong to the domain).

Request body

Name Description
amount Credit to add (above zero) or to take back (below zero), 100000 at most in absolute value, not zero (required).
description Label shown in the ledger of the organization (optional, 200 characters at most; default "Ajout manuel de crédit" or "Correction manuelle").
reason Why, kept in the audit journal only (300 characters at most; required for a correction).

Response: success: true. new_balance: Balance after the operation. transaction_id: Ledger row written.

POST /api/v1/domains/<domain_id>/organizations/<organization_id>/grace-extension

Force a longer grace period for an organization whose balance is negative (journalled).

The organization goes back to negative_grace whatever its state: a suspended account is put back in service (numbers resume or return) and its grace period now ends days days from now. The reminders and the suspension notice are armed again for the new end.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
organization_id Organization ID.

Request body

Name Description
days Days from now, 1 to 90 (required).
reason Why, kept in the audit journal (optional text of 300 characters at most).

Response: billing_state: {balance_state, negative_since, grace_ends_at, suspended_at, limbo_at, release_at, released_at, credit_balance}. 400 validation_error with field when days is out of bounds or the balance is not negative.

GET /api/v1/domains/<domain_id>/phone-numbers

List phone numbers in a domain.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: phone_numbers: The numbers of the domain (pool and organizations), each with organization_name when it belongs to an organization; provider_account_id = its SMS provider account (null: the provider settings of the domain); for a domain admin, a number bound to a platform account shows the provider "platform". total: Number of numbers.

POST /api/v1/domains/<domain_id>/phone-numbers

Create phone number(s) in domain pool (batch support).

Always answers 201 once the domain exists, even when no number was created: read created_count and errors (a number that already exists, or that cannot be stored, is listed there and the others are still created). The numbers are created in the pool of the domain, without a team. The provider configuration of a number is not set here (PUT .../phone-numbers/<phone_id>/provider).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
numbers List of phone numbers in E.164 format.
number Single phone number (legacy, use numbers instead).
provider SMS provider name (optional); it must be enabled for the domain, else 400 validation_error.
provider_account_id SMS provider account of the numbers (GET /domains/<id>/sms-accounts: an account of the domain or a platform account allowed for it; 400 validation_error otherwise). It sets the provider; provider is then ignored.

Response: phone_numbers: The numbers created. created_count: How many were created. errors: One string per number that was not created ("<number>: already exists", or the reason). message: Confirmation with the count.

DELETE /api/v1/domains/<domain_id>/phone-numbers/<phone_id>

Delete a phone number from domain pool.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
phone_id Phone number ID.

Response: Success message.

PUT /api/v1/domains/<domain_id>/phone-numbers/<phone_id>

Update a phone number in domain pool.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
phone_id Phone number ID.

Request body

Name Description
label Friendly label (no control character or line break: the label can become the caller name of the calls; 400 validation_error otherwise).
provider SMS provider name (refused with 400 validation_error while the number is bound to an account: the account decides).
provider_account_id SMS provider account of the number (an account of the domain or a platform account allowed for it, GET /domains/<id>/sms-accounts; 400 validation_error otherwise); null unbinds (back to the provider settings of the domain).
provider_config Provider configuration.
is_active Whether number is active, a boolean (400 validation_error otherwise). An inactive number is not charged the monthly fees and cannot send.
voice_trunk_id Voice trunk of the number, a trunk of this domain or a platform trunk allowed for it (400 validation_error otherwise; null turns the voice off). Super admin only for a number of the platform (bound to a platform SMS account, or whose current or new trunk is a platform trunk): 403 forbidden for a domain admin, the platform sets it in the Providers page (PUT /system/phone-numbers/<id>/voice).
purpose "access" makes the number an access number of the call codes (E23, legacy without interface; read by the voice server), null removes it. An access number must belong to the pool (no organization), have the voice enabled and a country.
country ISO 3166-1 alpha-2 country of an access number (CA, US...).

Response: phone_number: The updated number. message: Confirmation.

POST /api/v1/domains/<domain_id>/phone-numbers/<phone_id>/assign

Assign a phone number to an organization.

The number goes to the default team of the organization (flag is_default; the only team of an organization counts as default), created when the organization has no team yet, so that SMS work at once. An organization with several teams and none flagged default keeps the number without team: its admin assigns it (POST /phone-numbers/<id>/assign). Unassigning (organization_id null) also clears the team.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
phone_id Phone number ID.

Request body

Name Description
organization_id Organization ID to assign (null to unassign).

Response: phone_number: The number, with team_id set to the default team when there is one. message: Result message.

Errors 409 number_order_pending: the number was ordered from the provider and is not delivered yet.

PUT /api/v1/domains/<domain_id>/phone-numbers/<phone_id>/assigned-at

Update the organization assignment date for a phone number.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
phone_id Phone number ID.

Request body

Name Description
assigned_at ISO date string or null.

Response: phone_number: The updated number.

PUT /api/v1/domains/<domain_id>/phone-numbers/<phone_id>/provider

Configure SMS provider for a phone number.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
phone_id Phone number ID.

Request body

Name Description
provider SMS provider name.
config Provider configuration.
test_first Whether to test before saving (default true).

Response: phone_number: The updated number. message: Confirmation.

Errors 400 validation_error: provider missing. 400 provider_error: the connection test failed (skip it with test_first false). 404 not_found: domain or number unknown.

GET /api/v1/domains/<domain_id>/platform-voice-trunks

Which platform voice trunks a domain may use (super admin only).

Access: platform permission platform_admin

Path parameters

Name Description
domain_id Domain ID.

Response: voice_trunks: Every platform trunk {id, name, description, provider, host, enabled, allowed (whether the domain may use it), numbers (how many numbers of the domain name it)}.

PUT /api/v1/domains/<domain_id>/platform-voice-trunks

Set which platform voice trunks a domain may use (super admin only).

Access: platform permission platform_admin
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
trunk_ids Ids of the platform trunks the domain may use (the whole list; [] = none).

Response: voice_trunks: As GET.

Errors 400 validation_error: an id is not a platform trunk. 409 conflict: a trunk taken away is still named by numbers of the domain (trunk_id, numbers in the answer).

GET /api/v1/domains/<domain_id>/security-settings

Security settings of a domain: who must sign in with a second factor.

A second factor (authenticator app or passkey) is expected of the platform and domain admins; this setting adds the organization admins (members holding members.admin) of the domain's organizations. While the platform's MFA_ENFORCE is off (today) it is only recommended: the sign-in carries mfa_setup, nothing is refused (mfa_enforced false; the page says it will become mandatory). Once enforced (mfa_enforced true), with a grace period (MFA_GRACE_DAYS, 14 days by default) counted from the first time the account was seen needing one.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: security: mfa_organization_admins (boolean, false by default), mfa_enforced (boolean, read only: the platform imposes the second factor; false = only recommended), mfa_grace_days (the platform's grace period once enforced, read only).

PUT /api/v1/domains/<domain_id>/security-settings

Change the security settings of a domain.

Journaled (audit_logs, domain_security_changed). Never through an API key.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
mfa_organization_admins The organization admins of the domain must have a second factor too (required).

Response: security: The settings after the change (as GET).

GET /api/v1/domains/<domain_id>/settings/branding

Get the branding (theme) of a domain.

Every key is present; null (or the historical default) when unset: each application then uses its own default. Nothing in it is secret: the same object is in GET /domains/current.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: branding: {accent_color_fr, accent_color_en, logo_url, logo_signin_url, favicon_url, theme {primary, primary_text, navbar_bg, navbar_fg, body_bg, font, radius}, support {help_url, status_url, tickets_url, updates_url}, footer_text, tagline {fr, en}}, see PUT /domains/<domain_id>/settings/branding.

Errors 404 not_found: domain unknown.

PUT /api/v1/domains/<domain_id>/settings/branding

Update the branding (theme) of a domain: how every application served by the domain looks.

Partial update: only the keys sent change. theme and support merge key by key; a key sent as null (or an empty string) is removed, theme or support sent as null removes all their keys. Unset = the default of each application. Every value is checked (400 validation_error with field = its path, e.g. theme.primary): colors are #rgb or #rrggbb, stored as lowercase #rrggbb; URLs are https:// (never javascript:, data: or http:), 500 characters at most; an unknown key of theme or support is refused. Each change is journaled (audit_logs, domain_branding_changed, the names of the keys sent).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
accent_color_fr Accent color for the French locale, #rgb or #rrggbb (null or empty: back to #003DA5).
accent_color_en Accent color for the English locale, #rgb or #rrggbb (null or empty: back to #D52B1E).
logo_url Logo of the domain, an https:// URL or an absolute path starting with / (null or empty: none).
logo_signin_url Logo of the sign-in page, an https:// URL or an absolute path starting with / (null: removed).
favicon_url Favicon, an https:// URL or an absolute path starting with / (null: removed).
theme Colors and shape of the interface, merged key by key (null: all removed): primary (brand color: buttons, links, underlines), primary_text (text on primary; null: contrast computed by the client), navbar_bg, navbar_fg, body_bg (colors #rgb or #rrggbb), font (one of source-sans, system, inter, roboto), radius (corner radius in pixels, an integer from 0 to 16).
support Links of the ASSISTANCE menu, https:// URLs merged key by key (null: all removed): help_url, status_url, tickets_url, updates_url.
footer_text Footer, plain text of 500 characters at most (line breaks allowed, no HTML tag; null: removed).
tagline Line under the name on the sign-in page, per language, merged key by key (null: all removed): fr, en (one line of plain text, 120 characters at most; null: the app's own).
home What the web app puts first for the domain (its first page after sign-in and the top of its menu): messages, numbers or calls (null: messages). Another value gets 400 validation_error with field home.

Response: branding: The whole branding after the change, as GET /domains/<domain_id>/settings/branding. message: Confirmation.

Errors 400 validation_error: a value is refused (field = its path), or the body is not a JSON object. 404 not_found: domain unknown.

PUT /api/v1/domains/<domain_id>/settings/default-payment-provider

Set default payment provider for a domain.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
provider Provider name (stripe, nuvei).

Response: default_payment_provider: The provider now offered first. message: Confirmation.

GET /api/v1/domains/<domain_id>/settings/oauth

Get OAuth settings for a domain.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: oauth: {google, microsoft, apple}. Google and Microsoft: {enabled} only (sign-in uses the platform's credentials, the domain only turns it on). Apple: {enabled, client_id, team_id, key_id, private_key}, the private key masked as "**".

PUT /api/v1/domains/<domain_id>/settings/oauth/<provider>

Turn an OAuth provider on or off for a domain, and set the domain's Apple credentials.

Google and Microsoft sign in with the platform's credentials: the domain only turns them on (enabled), any other field of the body is ignored. Apple uses the domain's credentials and is replaced as a whole: a field left out takes its default (enabled false, empty strings); the private key is kept when it is left out, empty or sent as "**". The other providers are left alone.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.
provider Provider name (google, microsoft, apple).

Request body

Name Description
enabled Whether the provider is offered on the domain (default false).
client_id Apple only: Services ID.
team_id Apple only: team ID.
key_id Apple only: key ID.
private_key Apple only: private key (omitted, empty or "**" keeps the stored one).

Response: google | microsoft | apple: The settings of the provider after the change, as GET /domains/<domain_id>/settings/oauth shows them, under the name of the provider in the URL. message: Confirmation.

GET /api/v1/domains/<domain_id>/settings/payment-providers

Get payment provider settings for a domain.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: payment_providers: Settings of each payment provider, secrets masked. default_payment_provider: The provider offered first.

PUT /api/v1/domains/<domain_id>/settings/payment-providers/<provider>

Update payment provider settings for a domain.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.
provider Provider name (stripe, nuvei; fake, the offline test provider, on development and test domains only: 400 validation_error elsewhere).

Request body

Name Description
enabled Whether the provider is enabled (a field left out keeps its stored value).
publishable_key Stripe only: publishable key.
secret_key Stripe and Nuvei: secret key (omitted, empty or "**" keeps the stored one).
webhook_secret Stripe only: webhook secret (omitted, empty or "**" keeps the stored one). Nuvei has none to send: its notification secret is generated.
merchant_id Nuvei only: merchant ID.
merchant_site_id Nuvei only: merchant site ID.
environment Nuvei only: "production" (default) or "sandbox"; anything else is 400 validation_error with field environment.

Response: stripe | nuvei | fake: The settings of the provider after the change, under the name of the provider in the URL (secret_key and webhook_secret masked as "**"). message: Confirmation.

GET /api/v1/domains/<domain_id>/settings/smtp

Get SMTP settings for a domain.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: smtp: {host, port, username, password ("**" when set), from_email, from_name, use_tls}; an empty object when never configured.

PUT /api/v1/domains/<domain_id>/settings/smtp

Update SMTP settings for a domain.

The settings are replaced as a whole: a field left out takes its default (port 587, use_tls true, an empty string for the others, from_name = display name of the domain), it does not keep its stored value. Only the password is kept when it is left out, empty or sent as "**" (the masked value GET returns).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
host SMTP server hostname (in production a domain admin cannot give a private, loopback or link-local address: 400 validation_error; a host already saved is not re-checked).
port SMTP port, 1 to 65535 (default 587; 400 validation_error otherwise).
username SMTP username.
password SMTP password (omitted, empty or "**" keeps the stored one).
from_email From email address.
from_name From name (default: the display name of the domain).
use_tls Whether to use TLS (default true).

Response: smtp: The configuration after the change (password masked as "**", empty when none). message: Confirmation.

POST /api/v1/domains/<domain_id>/settings/smtp/test

Send a test email through the SMTP settings of a domain.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
to_email Address the test email goes to (required).

Response: success: Whether the email was sent. message: Confirmation, or the SMTP error (400).

GET /api/v1/domains/<domain_id>/sms-logs

Get SMS logs for a domain.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Query parameters

Name Description
direction Filter by direction (inbound/outbound).
account_id Only the entries of the numbers bound to this SMS provider account (a platform account the domain may use, or one of its own; 400 validation_error otherwise).
provider Filter by provider type (super admin only; ignored for anyone else, it would name the provider of a platform account).
start_date Filter by start date (ISO format).
end_date Filter by end date (ISO format).
skip Number of records to skip (default 0; a value that is not an integer is refused: 400 validation_error).
limit Maximum records to return (default 50, max 100; a value that is not an integer is refused: 400 validation_error).

Response: logs: The log entries of the page, each with provider_account_id and provider_description (the account of its number as the domain sees it: a platform account by the description the super admin wrote, never by its name; both null for a number bound to no account) and provider (reads "platform" for a domain admin when the account is the platform's). total: Number of entries before skip and limit. skip: Offset applied. limit: Page size applied.

GET /api/v1/domains/<domain_id>/sms-providers

Get SMS provider settings for a domain.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: sms_providers: Settings of each SMS provider of the domain, secrets masked.

PUT /api/v1/domains/<domain_id>/sms-providers/<provider>

Update SMS provider settings for a domain.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.
provider Provider type (one of GET /api/v1/providers).

Request body

Name Description
enabled Whether provider is enabled.
api_key API key (providers that use one; see GET /api/v1/providers/<name>/schema).
sms_token SMS token (providers that use one).
mms_account_id MMS account ID (providers that handle MMS).
webhook_secret Webhook secret (providers that deliver to a secret URL).

Response: message: Confirmation. provider: The provider name. enabled: Whether the provider is enabled after the change.

GET /api/v1/domains/<domain_id>/voice-billing

Voice billing settings of a domain (E22).

Billing is off until a domain admin turns it on: while enabled is false no call is debited and call codes are only limited by max_call_seconds.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: voice_billing: {enabled, currency, default_rate ({rate_per_min, connection_fee, increment} or null), max_call_seconds, safety_margin_seconds, reserve}.

PUT /api/v1/domains/<domain_id>/voice-billing

Change the voice billing settings of a domain (the fields given are changed).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
enabled Bill the outbound voice calls of the domain's organizations (boolean).
default_rate Rate for the destinations no prefix covers: {rate_per_min, connection_fee, increment} or null.
max_call_seconds Longest call allowed, 60 to 86400 (default 7200).
safety_margin_seconds Seconds taken off the duration the balance allows, 0 to 600 (default 10).
reserve Part of the balance a call may never use (default 0).
currency CAD (the only credit currency).

Response: voice_billing: The settings after the change.

POST /api/v1/domains/<domain_id>/voice-billing/rate-calls

Rate and debit now the voice calls that wait for it (the periodic task does the same).

Idempotent: a call already rated is never debited again. Only the calls of the domain's organizations are looked at.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
call_id Rate only this call (optional).
limit Most calls looked at, 1 to 1000 (default 200).
dry_run Compute the outcome and write nothing (boolean, default false).

Response: result: {examined, billed, free, not_billed, insufficient, failed, deferred, skipped, error, total_cost}.

GET /api/v1/domains/<domain_id>/voice-rates

Voice rates of a domain, by direction then prefix.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Query parameters

Name Description
direction outbound or inbound (optional).
prefix Only the rates whose prefix starts with these digits (optional).
skip Pagination offset (default 0).
limit Page size (default 50, brought back into 1-100).

Response: rates: The page of rates {id, domain_id, prefix, direction, rate_per_min, connection_fee, increment, currency, label, enabled, created_at, updated_at}. total: Number of rates matching the filters. skip: Offset applied. limit: Page size applied.

POST /api/v1/domains/<domain_id>/voice-rates

Add a voice rate to a domain.

A rate bills the destinations whose number starts with prefix; the longest prefix wins. Example: prefix 1 (North America) 0.02 per minute, prefix 1514 (Montreal) 0.015, prefix 44 (United Kingdom) 0.05.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
prefix Destination prefix in digits, country code first, no + (required).
rate_per_min Price of a minute (required).
connection_fee Fixed price added to every billed call (default 0).
increment Billing step in seconds, 1 to 3600 (default 60).
direction outbound (default) or inbound (inbound calls are not billed yet).
label Name of the destination (optional).
enabled A disabled rate is ignored (default true).
currency CAD.

Response: rate: The created rate. 409 conflict when the prefix already has a rate in this direction.

DELETE /api/v1/domains/<domain_id>/voice-rates/<rate_id>

Delete a voice rate (the calls it already billed keep their cost).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
rate_id Rate ID.

Response: success: True.

PUT /api/v1/domains/<domain_id>/voice-rates/<rate_id>

Change a voice rate (the fields given are changed).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
rate_id Rate ID.

Request body

Name Description
prefix Destination prefix.
rate_per_min Price of a minute.
connection_fee Fixed price per billed call.
increment Billing step in seconds.
direction outbound or inbound.
label Name of the destination.
enabled Whether the rate applies.

Response: rate: The rate after the change. 409 conflict when the new prefix already has a rate.

POST /api/v1/domains/<domain_id>/voice-rates/bulk

Create or replace many voice rates at once (a tariff table).

All the rates are validated first: one invalid rate refuses the whole request. A rate whose direction and prefix already exist replaces the amounts of that rate.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
rates List of up to 500 rates, with the fields of POST /voice-rates.

Response: created: Number of rates created. updated: Number of rates replaced.

GET /api/v1/domains/<domain_id>/voice-rates/resolve

The rate that would bill a call to a destination (longest prefix, else the default rate).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Query parameters

Name Description
destination Destination number (required).

Response: rate: {source ("prefix" or "default"), rate_id, prefix, rate_per_min, connection_fee, increment, currency}, or null when nothing covers the destination. destination: The destination in E.164, or null when it is not a phone number.

GET /api/v1/domains/<domain_id>/voice-sound-limits

Limits applied to the audio prompts uploaded in the organizations of a domain.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: voice_sound_limits: {max_per_account (20), min_duration_s (1), max_duration_s (90), max_bytes (5 MB), formats (wav, mp3, m4a, ogg, webm)}.

PUT /api/v1/domains/<domain_id>/voice-sound-limits

Change the limits of the audio prompts of a domain (the fields given are changed; the existing sounds are kept).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
max_per_account Sounds an organization may keep, 1 to 500 (the system sounds do not count).
min_duration_s Shortest sound in seconds, 0 to 30.
max_duration_s Longest sound in seconds, 1 to 600.
max_bytes Largest file, 10 KB to 12 MB.
formats Accepted formats, among wav, mp3, m4a, ogg, webm.

Response: voice_sound_limits: The limits after the change.

GET /api/v1/domains/<domain_id>/voice-sounds

System sounds of a domain, the default of every organization that has not recorded its own.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: sounds: List of {id, name, kind "system", key, language, duration_s, size...}. keys: The keys that exist (filtering, name_prompt, invalid_key, voicemail_greeting, paused, recording). languages: fr, en and es.

POST /api/v1/domains/<domain_id>/voice-sounds

Upload the system sound of a key and a language for the whole domain (replaces the previous one). Same checks as the sounds of an organization: formats, size, measured duration.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
file The audio file (multipart/form-data).
key filtering, name_prompt, invalid_key, voicemail_greeting, paused or recording.
language fr, en or es.
name Name (default: the file name).

Response: sound: The created sound.

DELETE /api/v1/domains/<domain_id>/voice-sounds/<sound_id>

Delete a system sound of the domain (the voice servers fall back on their built-in sound).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
sound_id Sound ID.

Response: success: True.

GET /api/v1/domains/<domain_id>/voice-sounds/<sound_id>/file

The audio file of a system sound of the domain.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
sound_id Sound ID.

Response: The file (audio/*).

GET /api/v1/domains/<domain_id>/voice-trunks

The voice trunks a domain can give its numbers.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: voice_trunks: The domain's own trunks, with their credentials (password masked) and numbers (how many numbers name each). platform_trunks: The platform trunks the super admin allowed for the domain: for a domain admin only {id, scope "platform", name null, provider null, description, enabled, outbound_enabled} (description = what the super admin wrote); a super admin also gets the name, host and provider. own_trunks_allowed (boolean): Whether the domain's admins may add their own trunks (the "own providers" permission of the domain, PUT /domains/<id>/sms-account-settings; off by default).

POST /api/v1/domains/<domain_id>/voice-trunks

Create a voice trunk of the domain (a reseller's own trunk).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
name Trunk name (unique), 1 to 64 characters: no control character or line break, no double quote, backslash, comma, ampersand, pipe, dollar sign, brace or backtick (the name is written into the voice server's commands); 400 validation_error with field otherwise.
host SIP server hostname or IP address (400 validation_error otherwise; in production a domain admin cannot give a private, loopback or link-local address; a public provider address is fine).
username SIP username (a string without control character).
password SIP password.
provider Name of the provider (free text, for the administrators).
description Free description of the trunk (up to 200 characters).
port SIP port (default: 5060).
server_id Voice server that registers this trunk; default: the least loaded enabled server.
trunk_type "registration" (default) or "ip_auth" (the provider identifies us by IP).
outbound_enabled Calls may go out through it (default: yes for registration, no for ip_auth).
inbound_enabled Calls may come in through it (default: yes; false: outbound only; not both false).
outbound_trunk_id A trunk without outbound: another trunk of the same owner, open to outbound, that its outbound calls and forwards take instead (empty: none).
max_channels Simultaneous calls it carries, inbound and outbound together (empty: no limit).
allowed_ips ip_auth only: other IP addresses the carrier calls from (list, or one per line); the host is always accepted.

Response: voice_trunk: The created trunk, with its credentials. message: Confirmation.

Errors 403 own_accounts_not_allowed: the domain may not add its own providers (PUT /domains/<id>/sms-account-settings), whoever asks; platform trunks are created under /system/voice-trunks.

DELETE /api/v1/domains/<domain_id>/voice-trunks/<trunk_id>

Delete a voice trunk of the domain.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.
trunk_id Trunk ID.

Response: Success message.

PUT /api/v1/domains/<domain_id>/voice-trunks/<trunk_id>

Update a voice trunk of the domain.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.
trunk_id Trunk ID (a trunk of this domain; a platform trunk answers 404).

Request body

Name Description
name Trunk name (same rules as at the creation, cannot be empty).
description Free description of the trunk.
host SIP server hostname or address (same rule as at creation; the host the trunk already has is not re-checked).
username SIP username.
password SIP password.
port SIP port.
enabled Whether trunk is enabled.
server_id Voice server that registers this trunk.
outbound_enabled Calls may go out through it (outbound calls, forwards).
inbound_enabled Calls may come in through it (false: outbound only; not both false).
outbound_trunk_id A trunk without outbound: another trunk of the domain, open to outbound, that its outbound calls and forwards take instead (empty: none).
max_channels Simultaneous calls it carries, inbound and outbound together (empty: no limit).
allowed_ips ip_auth only: other IP addresses the carrier calls from.
static_config false hands a trunk still marked as defined in the voice server's static configuration to the database (true is ignored).

Response: voice_trunk: The updated trunk, with its credentials. message: Confirmation.

GET /api/v1/domains/<domain_id>/voice-trunks/status

Get the state of the voice trunks, from the voice server.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: registrations: State of each own trunk of the domain, by trunk name: Registered, Rejected... for a registration trunk; Reachable, Unreachable or Unknown for an IP trunk (the voice server checks it regularly). platform_registrations: State of the platform trunks the domain may use, by trunk id (their names stay hidden).

GET /api/v1/domains/<domain_id>/webhook

Get webhook information for a domain.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: webhook_secret: The secret of the domain webhook. webhook_urls: Path of the domain-level address of each enabled SMS provider, /webhook/domain/<provider>/<secret> (still served; prefix it with the API address). The current form, /webhook/provider/<provider>/<secret>, is the one of the global provider webhooks (POST /api/v1/system/provider-webhooks).

POST /api/v1/domains/<domain_id>/webhook/regenerate

Regenerate webhook secret for a domain.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Response: webhook_secret: The new secret (the old URLs stop working). webhook_urls: The new paths, as in GET .../webhook. message: Confirmation.

GET /api/v1/domains/current

Get current domain info based on host header.

Public, never rate limited (a read of the domain resolved for every request anyway).

Access: public (no authentication)
Rate limit: exempt from the general per-address limit

Response: Public domain information.

Errors 404 domain_not_found: no domain matches X-Frontend-Host.

System

Global SMTP, OAuth and provider webhooks.

GET /api/v1/system/billing-overview

What every domain bills its organizations this month, summed up by domain (platform administrators).

Access: platform permission platform_admin

Response: month: The current calendar month {start, end}. domains: [{id, name (display name), organizations (how many), subscriptions (active subscriptions), this_month (debits since the 1st), communications (usage debited since the 1st), previous_month (debits of the last calendar month), next (next statements of the subscriptions, estimated; 0 when the domain does not offer subscriptions)}]. totals: {this_month, communications, previous_month, next}.

GET /api/v1/system/oauth

Get OAuth provider settings for current domain.

Access: platform permission platform_admin

Response: oauth: As GET /domains/<domain_id>/settings/oauth: Google and Microsoft {enabled}, Apple its credentials (private key masked).

Errors 404 domain_not_found: no domain matches X-Frontend-Host.

PUT /api/v1/system/oauth/<provider>

Turn an OAuth provider on or off for the current domain, and set its Apple credentials (as PUT /domains/<domain_id>/settings/oauth/<provider>).

Google and Microsoft sign in with the platform's credentials: only enabled is read. Apple is replaced as a whole, the private key kept when it is left out, empty or "**". 404 domain_not_found when no domain matches X-Frontend-Host.

Access: platform permission platform_admin
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
provider Provider name (google, microsoft, apple).

Request body

Name Description
enabled Whether the provider is offered on the domain (default false).
client_id Apple only: Services ID.
team_id Apple only: team ID.
key_id Apple only: key ID.
private_key Apple only: private key (omitted, empty or "**" keeps the stored one).

Response: google | microsoft | apple: The settings of the provider after the change, under the name of the provider in the URL. message: Confirmation.

GET /api/v1/system/provider-webhooks

List all provider webhooks.

Access: platform permission platform_admin

Response: webhooks: The global provider webhooks. total: Number of webhooks.

POST /api/v1/system/provider-webhooks

Create a new provider webhook.

Access: platform permission platform_admin

Request body

Name Description
provider Provider type (one of GET /api/v1/providers).
display_name Display name.
secret Webhook secret (optional, generated if not provided).
enabled Whether the webhook is enabled.

Response: webhook: The created webhook (with its secret). message: Confirmation.

DELETE /api/v1/system/provider-webhooks/<webhook_id>

Delete a provider webhook.

Access: platform permission platform_admin

Path parameters

Name Description
webhook_id Webhook ID.

Response: Success message.

PUT /api/v1/system/provider-webhooks/<webhook_id>

Update a provider webhook.

Access: platform permission platform_admin

Path parameters

Name Description
webhook_id Webhook ID.

Request body

Name Description
display_name Display name (optional).
enabled Whether the webhook is enabled (optional).
config Provider-specific configuration (optional).

Response: webhook: The updated webhook. message: Confirmation.

POST /api/v1/system/provider-webhooks/<webhook_id>/regenerate

Regenerate provider webhook secret.

Access: platform permission platform_admin

Path parameters

Name Description
webhook_id Webhook ID.

Response: webhook: The webhook with its new secret. message: Confirmation.

GET /api/v1/system/smtp

Get SMTP settings of the current domain (the domain of the host, X-Frontend-Host); a domain admin only for a domain they administer (403 forbidden otherwise, 404 domain_not_found without a domain).

Access: platform permission platform_admin or domain_admin

Response: smtp: {host, port, username, password ("**" when set), from_email, from_name, use_tls}; the defaults when never configured.

PUT /api/v1/system/smtp

Update SMTP settings of the current domain (the domain of the host, X-Frontend-Host); a domain admin only for a domain they administer (403 forbidden otherwise, 404 domain_not_found without a domain).

The settings are replaced as a whole: a field left out takes its default (port 587, use_tls true, an empty string for the others, from_name = display name of the domain), it does not keep its stored value. Only the password is kept when it is left out, empty or sent as "**".

Access: platform permission platform_admin or domain_admin
API keys: refused, 403 forbidden (API key not allowed on this route)

Request body

Name Description
host SMTP server host.
port SMTP server port (default 587).
username SMTP username.
password SMTP password (omitted, empty or "**" keeps the stored one).
from_email From email address.
from_name From name (default: the display name of the domain).
use_tls Whether to use TLS (default true).

Response: smtp: The configuration after the change (password masked). message: Confirmation.

POST /api/v1/system/smtp/test

Test SMTP connection of the current domain (the domain of the host, X-Frontend-Host); a domain admin only for a domain they administer (403 forbidden otherwise, 404 domain_not_found without a domain).

Access: platform permission platform_admin or domain_admin

Request body

Name Description
to_email Email address to send test to.

Response: Success or error message.

GET /api/v1/system/voice-security

Security of the voice platform: the switch that suspends every outbound call, the organizations whose outbound calls are suspended and the latest security events.

Access: platform permission platform_admin

Response: state: {outbound_suspended, at (last change, ISO 8601, null before any), by (ID of the account that changed it), note}. suspended: The organizations whose outbound calls are suspended, 500 at most [{organization_id, name, domain_id, suspension {reason (spend, call_cost, manual), detail, at, by}}]. events: The latest 100 security events [{id, organization_id, organization_name, type, detail, at, notified}].

PUT /api/v1/system/voice-security

Suspend or resume every outbound call of the platform (calls of the endpoints, forwards, call codes). The calls in progress are hung up within seconds; incoming calls go on. Journalled; the platform admins receive an alert.

Access: platform permission platform_admin

Request body

Name Description
outbound_suspended true suspends, false resumes (required).
note Why (optional string, cut at 500 characters).

Response: state: {outbound_suspended, at (ISO 8601), by (ID of the account), note}.

GET /api/v1/system/voice-servers

Voice servers the SIP endpoints and trunks are placed on.

Access: platform permission platform_admin or domain_admin

Response: voice_servers: List of servers with their endpoint count.

POST /api/v1/system/voice-servers

Declare a voice server.

Access: platform permission platform_admin

Request body

Name Description
name Unique short name, the one the voice server is configured with (e.g. "voice1").
host IP address or hostname the apps and phones register on.
port SIP port (default 5060).
transport "udp" (default), "tcp" or "tls".
description Free text (optional).
enabled Whether new endpoints may be placed on it (default true).
listeners {udp, tcp, tls: {enabled, port}}: what the server listens on (default UDP 5060 alone). The transport and port given to the apps must be one that is on (400 validation_error, field transport).
tls {cert_file, key_file}: certificate and key of the TLS listener, paths on the voice server (default: the certificate the deployment installs there).
external_address Public IP address or host name the server announces behind a NAT (empty: none).
bind_address Local IPv4 address the voice listens on and sends SIP and RTP from (empty: every address). Needed with an additional (movable) IP given to providers that authenticate by address.
local_nets Networks reached without the NAT (list of CIDR, e.g. ["10.0.0.0/8"]).
api_url Address of the management API of the voice server, as this platform reaches it (e.g. http://10.1.1.180:8090 on the private network; http or https, a host and a port, nothing else). Empty: the voice server on the machine of the platform. Its state, its restart, the state of its endpoints and trunks and the reloads go there.

Response: voice_server: The created server.

DELETE /api/v1/system/voice-servers/<server_id>

Delete a voice server that has no endpoint nor trunk left.

Access: platform permission platform_admin

Path parameters

Name Description
server_id Server ID.

Response: success: True.

PUT /api/v1/system/voice-servers/<server_id>

Update a voice server. Endpoints already placed on it keep its new address at their next provisioning.

Access: platform permission platform_admin

Path parameters

Name Description
server_id Server ID.

Request body

Name Description
name Fields to change (all optional; see POST). The voice server applies listeners, TLS, address and networks at its next start (POST .../restart).
host Fields to change (all optional; see POST). The voice server applies listeners, TLS, address and networks at its next start (POST .../restart).
port Fields to change (all optional; see POST). The voice server applies listeners, TLS, address and networks at its next start (POST .../restart).
transport Fields to change (all optional; see POST). The voice server applies listeners, TLS, address and networks at its next start (POST .../restart).
description Fields to change (all optional; see POST). The voice server applies listeners, TLS, address and networks at its next start (POST .../restart).
enabled Fields to change (all optional; see POST). The voice server applies listeners, TLS, address and networks at its next start (POST .../restart).
listeners Fields to change (all optional; see POST). The voice server applies listeners, TLS, address and networks at its next start (POST .../restart).
tls Fields to change (all optional; see POST). The voice server applies listeners, TLS, address and networks at its next start (POST .../restart).
external_address Fields to change (all optional; see POST). The voice server applies listeners, TLS, address and networks at its next start (POST .../restart).
bind_address Fields to change (all optional; see POST). The voice server applies listeners, TLS, address and networks at its next start (POST .../restart).
local_nets Fields to change (all optional; see POST). The voice server applies listeners, TLS, address and networks at its next start (POST .../restart).
api_url Fields to change (all optional; see POST). The voice server applies listeners, TLS, address and networks at its next start (POST .../restart).

Response: voice_server: The updated server.

POST /api/v1/system/voice-servers/<server_id>/restart

Write the server's SIP transports and restart it as soon as no call is up (it reads its transports at start only).

Access: platform permission platform_admin

Path parameters

Name Description
server_id Server ID.

Response: status: restart_scheduled (the server restarts once its calls have ended; registered phones come back by themselves).

Errors 502 voice_server_unreachable: the voice server did not answer, or the management API reached is the one of another voice server (nothing is restarted).

GET /api/v1/system/voice-servers/<server_id>/runtime

What the voice server really runs, asked to it: version, SIP transports, audio (RTP) ports, time zone, active calls, and whether its saved settings wait for a restart.

Access: platform permission platform_admin

Path parameters

Name Description
server_id Server ID.

Response: runtime: {server (its name), version, timezone, transports [{name, protocol, bind}], expected_transports, rtp {start, end}, active_calls, restart_needed, ami_connected}, or null when the voice server does not answer. reachable (boolean): Whether the voice server answered.

GET /api/v1/system/voice-settings

Settings of the voice platform, applied by every voice server within 30 seconds.

Access: platform permission platform_admin

Response: voice_settings: {push_wait_seconds (longest wait, after the push that wakes the app, for the app to register again: the endpoint rings as soon as it did, 0 to 15), max_contacts (devices registered at once on one endpoint, 1 to 10, default 3: the app on a phone or a tablet, a SIP desk phone; they all ring together; beyond, the oldest registration is replaced; an endpoint may set its own), inbound_trunk_check (off, warn or enforce: a call that comes in on a trunk not allowed for the number is let through, logged, or refused), callcode {max_failures, failure_window (seconds), require_verified_phone}, test_extensions (600 echo and 601 hello world for the endpoints), updated_at}.

PUT /api/v1/system/voice-settings

Change settings of the voice platform (the fields given).

Access: platform permission platform_admin

Request body

Name Description
push_wait_seconds 0 to 15 (longest wait for the app woken by a push; 0 = ring at once).
max_contacts 1 to 10 (devices registered at once on one endpoint, unless the endpoint sets its own).
inbound_trunk_check off, warn or enforce.
callcode {max_failures (1 to 1000), failure_window (60 to 86400 seconds), require_verified_phone (boolean)}.
test_extensions Boolean.

Response: voice_settings: As GET.

Platform SMS provider accounts

Accounts at an SMS provider configured once and shared by the allowed domains (super admin).

GET /api/v1/system/sms-accounts

List the platform SMS provider accounts.

Access: platform permission platform_admin

Response: accounts: The platform accounts, secrets masked, each with numbers (how many numbers name it) and domains (the domains allowed to use it, [{id, name}]).

POST /api/v1/system/sms-accounts

Create a platform SMS provider account.

Access: platform permission platform_admin
API keys: refused, 403 forbidden (API key not allowed on this route)

Request body

Name Description
provider Provider type, one of GET /api/v1/providers (required).
name Name of the account (required; never shown to a domain admin).
description What the domain admins of the allowed domains see of this account (up to 200 characters), in place of its name and provider.
config Credentials, the fields of GET /api/v1/providers/<provider>/schema.
webhook_secret Secret of the inbound webhook URL (16 to 128 letters, digits, - and _); generated when absent.
enabled Whether the account sends and receives (default true).

Response: account: The created account (webhook_path = path of its inbound webhook).

DELETE /api/v1/system/sms-accounts/<account_id>

Delete a platform account that no domain and no number uses.

Access: platform permission platform_admin
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
account_id Account ID.

Response: message: Confirmation.

Errors 409 conflict: domains are allowed to use it or numbers name it (numbers, domains in the answer).

PUT /api/v1/system/sms-accounts/<account_id>

Update a platform SMS provider account.

Access: platform permission platform_admin
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
account_id Account ID.

Request body

Name Description
name New name.
description What the domain admins see of the account.
config Credentials to change (a secret left empty or masked keeps its value).
webhook_secret New secret of the inbound webhook URL (16 to 128 letters, digits, - and _); the old URL stops working at once.
enabled Whether the account sends and receives; off, the numbers bound to it cannot send.

Response: account: The updated account.

PUT /api/v1/system/sms-accounts/<account_id>/domains

Set the domains allowed to use a platform account (the same allowance as PUT /domains/<id>/platform-sms-accounts, seen from the account).

Access: platform permission platform_admin
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
account_id Account ID.

Request body

Name Description
domain_ids Ids of the domains that may use the account (the whole list; [] = none).

Response: account: The account with its domains.

Errors 400 validation_error: an id is not a domain. 409 conflict: a domain taken away still has numbers bound to the account (domain_id, numbers in the answer).

GET /api/v1/system/sms-accounts/<account_id>/numbers

The numbers of a platform account (its inventory), every domain included.

Access: platform permission platform_admin

Path parameters

Name Description
account_id Account ID.

Response: numbers: [{id, number, label, domain_id, domain_name, organization_id, organization_name, is_active, removable, assigned, held, pending_order ({status, placed_at} while the provider has not delivered the number, else null)}] (removable false while the number is assigned to an organization, in the trash, held for the balance or not delivered yet).

POST /api/v1/system/sms-accounts/<account_id>/numbers

Add numbers to a platform account: they are created in the pool of a domain allowed to use the account.

Access: platform permission platform_admin
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
account_id Account ID.

Request body

Name Description
domain_id Domain the numbers belong to (required; a domain allowed to use the account, else 400 validation_error).
numbers Phone numbers in E.164 (up to 500; a text with one number per line is accepted).
voice_trunk_id The voice trunk of the new numbers (a trunk the domain may use, else 400 validation_error); absent = no voice.

Response: created: The numbers created. created_count: How many. errors: One line per number not created ("<number>: already exists" or "not a phone number"). numbers: The inventory after the change.

DELETE /api/v1/system/sms-accounts/<account_id>/numbers/<phone_id>

Remove a number from a platform account (it leaves the registry).

Access: platform permission platform_admin
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
account_id Account ID.
phone_id Phone number ID.

Response: message: Confirmation. numbers: The inventory after the change.

Errors 404 not_found: not a number of this account. 409 number_assigned, number_held or number_order_pending: the number is assigned to an organization, in the trash, held for the balance, or not delivered by the provider yet.

POST /api/v1/system/sms-accounts/<account_id>/numbers/move

Move numbers of a platform account to another domain allowed to use the account (they stay in the registry).

A number assigned to an organization is moved only with release_assigned (it is then taken back from its organization, as by an unassignment: its conversations stay with the organization's team); a number in the trash or held for the balance is never moved. A moved number loses what belonged to its old domain: a voice trunk the new domain may not use (it then arrives without voice: give it a trunk by hand; a platform trunk allowed for the new domain, or no trunk at all, keeps the voice), access number role, a previous owner's settings.

Access: platform permission platform_admin
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
account_id Account ID.

Request body

Name Description
phone_ids Ids of the numbers to move (required).
domain_id Destination domain (required; a domain allowed to use the account, else 400 validation_error).
release_assigned Also move the numbers assigned to an organization, taking them back from it (default false).

Response: moved: The numbers moved. moved_count: How many. errors: One line per number not moved. numbers: The inventory after the change.

POST /api/v1/system/sms-accounts/<account_id>/regenerate-secret

Give a platform account a new inbound webhook secret (the old URL stops working at once: change it at the provider).

Access: platform permission platform_admin
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
account_id Account ID.

Response: account: The account with its new webhook secret.

POST /api/v1/system/sms-accounts/<account_id>/test

Test the connection of a platform account with its stored credentials.

Access: platform permission platform_admin

Path parameters

Name Description
account_id Account ID.

Response: success: Whether the provider accepted the credentials. message: The provider's answer.

SMS provider accounts of a domain

A domain's own accounts (reseller) and the platform accounts it may use; a number picks its account (provider_account_id).

GET /api/v1/domains/<domain_id>/platform-sms-accounts

Which platform SMS provider accounts a domain may use (super admin only).

Access: platform permission platform_admin

Path parameters

Name Description
domain_id Domain ID.

Response: accounts: Every platform account (no secrets) with allowed (whether the domain may use it) and numbers (how many numbers of the domain name it).

PUT /api/v1/domains/<domain_id>/platform-sms-accounts

Set which platform SMS provider accounts a domain may use (super admin only).

Access: platform permission platform_admin
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
account_ids Ids of the platform accounts the domain may use (the whole list; [] = none).

Response: accounts: As GET.

Errors 400 validation_error: an id is not a platform account. 409 conflict: an account taken away is still named by numbers of the domain (account_id, numbers in the answer).

PUT /api/v1/domains/<domain_id>/sms-account-settings

What a domain may do with SMS provider accounts (super admin only).

Access: platform permission platform_admin
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
own_accounts_allowed Whether the domain's admins may add their own provider accounts (off by default). Turning it off keeps the accounts already there.

Response: own_accounts_allowed: The value now in force.

GET /api/v1/domains/<domain_id>/sms-accounts

The SMS provider accounts a domain can bind its numbers to.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.

Response: accounts: The domain's own accounts, secrets masked, with numbers (how many numbers name each). platform_accounts: The platform accounts the super admin allowed for the domain: for a domain admin only {id, scope "platform", name null, provider null, description, enabled} (description = what the super admin wrote for them); a super admin also gets the name and provider. own_accounts_allowed (boolean): Whether the domain's admins may add their own provider accounts (set by a super admin; off by default).

POST /api/v1/domains/<domain_id>/sms-accounts

Create an SMS provider account of the domain (a reseller's own account).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
provider Provider type, one of GET /api/v1/providers (required).
name Name of the account (required).
config Credentials, the fields of GET /api/v1/providers/<provider>/schema.
webhook_secret Secret of the inbound webhook URL (16 to 128 letters, digits, - and _); generated when absent.
enabled Whether the account sends and receives (default true).

Response: account: The created account (webhook_path = path of its inbound webhook, to configure at the provider).

Errors 403 own_accounts_not_allowed: the domain may not add its own accounts (PUT /domains/<id>/sms-account-settings), whoever asks.

DELETE /api/v1/domains/<domain_id>/sms-accounts/<account_id>

Delete an account of the domain that no number names.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.
account_id Account ID.

Response: message: Confirmation.

Errors 409 conflict: numbers name it (numbers in the answer).

PUT /api/v1/domains/<domain_id>/sms-accounts/<account_id>

Update an SMS provider account of the domain.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.
account_id Account ID (an account of this domain; a platform account answers 404).

Request body

Name Description
name New name.
description Free description of the account.
config Credentials to change (a secret left empty or masked keeps its value).
webhook_secret New secret of the inbound webhook URL (16 to 128 letters, digits, - and _); the old URL stops working at once.
enabled Whether the account sends and receives.

Response: account: The updated account.

GET /api/v1/domains/<domain_id>/sms-accounts/<account_id>/numbers

The numbers of an account of the domain (its inventory).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
account_id Account ID (an account of this domain; a platform account answers 404).

Response: numbers: [{id, number, label, domain_id, domain_name, organization_id, organization_name, is_active, removable}].

POST /api/v1/domains/<domain_id>/sms-accounts/<account_id>/numbers

Add numbers to an account of the domain (created in the domain's pool).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.
account_id Account ID.

Request body

Name Description
numbers Phone numbers in E.164 (up to 500; a text with one number per line is accepted).

Response: created: The numbers created. created_count: How many. errors: One line per number not created. numbers: The inventory after the change.

DELETE /api/v1/domains/<domain_id>/sms-accounts/<account_id>/numbers/<phone_id>

Remove a number from an account of the domain (it leaves the registry).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.
account_id Account ID.
phone_id Phone number ID.

Response: message: Confirmation. numbers: The inventory after the change.

Errors 404 not_found: not a number of this account. 409 number_assigned or number_held: assigned to an organization, in the trash or held.

POST /api/v1/domains/<domain_id>/sms-accounts/<account_id>/regenerate-secret

Give an account of the domain a new inbound webhook secret (the old URL stops working at once).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.
account_id Account ID.

Response: account: The account with its new webhook secret.

POST /api/v1/domains/<domain_id>/sms-accounts/<account_id>/test

Test the connection of an account of the domain with its stored credentials.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)

Path parameters

Name Description
domain_id Domain ID.
account_id Account ID.

Response: success: Whether the provider accepted the credentials. message: The provider's answer.

Platform number providers

The platform's accounts at the providers that sell numbers (super admin); a domain admin only sees their description.

GET /api/v1/system/number-providers

List the number providers of the platform.

Access: platform permission platform_admin

Response: number_providers: [{id, provider, name, description, enabled, base_url, credentials {username, password (masked)}, sms_account_id, voice_trunk_id, costs {local_setup, local_monthly, port_in, currency}, port_in {trunks, reseller}, numbers (how many numbers were bought from it), capabilities}]. providers: The providers an account can be opened at.

POST /api/v1/system/number-providers

Add a number provider account.

Access: platform permission platform_admin

Request body

Name Description
provider The provider (one of providers in GET) (required).
name Name for the super admin (required); never shown to a domain admin.
description What the domain admins see of it (up to 200 characters).
base_url Base URL of its API (default: the provider's).
credentials {username, password}: the access to its API.
sms_account_id The platform SMS account, at the same provider, the numbers bought here send through (required). The domains allowed to use that SMS account may buy from this provider.
voice_trunk_id The platform voice trunk on which the provider delivers the calls (given to the numbers bought, for the domains allowed to use it).
costs {local_setup, local_monthly, port_in, currency (CAD or USD)}: what the provider charges us per number bought, per month and per number ported in (its API gives no price).
port_in {trunks: the provider's circuit ids where ported numbers land (a port request is refused without one), reseller: name sent with the port requests (optional)}.
enabled Default true.

Response: number_provider: The account (as in GET).

DELETE /api/v1/system/number-providers/<account_id>

Delete a number provider account no number was bought from (disable it otherwise).

Access: platform permission platform_admin

Path parameters

Name Description
account_id Account ID.

Response: message: Confirmation.

Errors 409 conflict: numbers were bought from it (numbers in the answer).

PUT /api/v1/system/number-providers/<account_id>

Change a number provider account (the fields given change; a password empty or masked is kept).

Access: platform permission platform_admin

Path parameters

Name Description
account_id Account ID.

Request body

Name Description
name As POST (provider cannot change).
description As POST (provider cannot change).
base_url As POST (provider cannot change).
credentials As POST (provider cannot change).
sms_account_id As POST (provider cannot change).
voice_trunk_id As POST (provider cannot change).
costs As POST (provider cannot change).
port_in As POST (provider cannot change).
enabled As POST (provider cannot change).

Response: number_provider: The account.

POST /api/v1/system/number-providers/<account_id>/test

Check the connection to a number provider (one read call).

Access: platform permission platform_admin

Path parameters

Name Description
account_id Account ID.

Response: ok: Whether the provider answered and accepted the credentials. message: What happened.

Platform providers

What the platform has at each provider, grouped by type: SMS accounts, trunks, number provider accounts, numbers and their capabilities; the voice trunk of a number (super admin).

GET /api/v1/system/did-inventory

The inventory of a number provider account: the numbers the provider says the account holds, read now, set against the numbers of Telma.

The provider gives only the numbers and whether SMS is on: the comparison is about existence and SMS. A number of Telma is at the provider when it was bought from it, sends through one of its SMS accounts or its calls come in through one of its trunks.

Access: platform permission platform_admin

Query parameters

Name Description
provider_id The number provider account (required).

Response: account_id: The account. read_at: When the provider was read (ISO 8601). provider_count: How many numbers the provider lists. counts: {matched, sms_mismatch, provider_only, ignored, elsewhere, ours_only, pending, duplicates}. matched: [{number, phone_id, label, domain_id, domain_name, organization_id, organization_name, is_active, provider_sms, our_sms, sms_mismatch}]: held at the provider and in Telma at this provider. provider_only: [{number, provider_sms}]: held at the provider, unknown to Telma (to import or to ignore). ignored: [{number, provider_sms, reason, at, by}]: numbers of provider_only left aside, with the reason. elsewhere: [{number, phone_id, label, domain_id, domain_name, organization_id, organization_name, is_active, provider_sms, provider}]: held at the provider, known to Telma but bound to another provider. ours_only: [{number, phone_id, label, domain_id, domain_name, organization_id, organization_name, is_active, our_sms, bought_here, missing_since}]: in Telma at this provider, not listed by it. pending: [{number, phone_id, label, domain_id, domain_name, organization_id, organization_name, is_active, pending_order, provider_sms}]: ordered and not delivered yet (not a gap). duplicates: [{number, count, ids}]: numbers held by more than one record of Telma.

POST /api/v1/system/did-inventory/ignored

Leave aside a number the account holds and Telma does not know (a number kept at the provider on purpose, to give back...), with the reason. Reversible. Journaled.

Access: platform permission platform_admin

Request body

Name Description
provider_id The number provider account (required).
number The number, in E.164 format (required).
reason Why it is left aside, 1 to 200 characters (required).

Response: ignored: {number, reason, at, by}.

DELETE /api/v1/system/did-inventory/ignored/<number>

Bring an ignored number back among the numbers to settle. Journaled.

Access: platform permission platform_admin

Path parameters

Name Description
number The number, in E.164 format.

Query parameters

Name Description
provider_id The number provider account (required).

Response: success: True.

POST /api/v1/system/did-inventory/import

Put numbers the account holds and Telma does not know into the pool of a domain, as if bought there.

Each number is bound to the account, its SMS account and its voice trunk (when the domain may use them), active and free in the pool; SMS is turned on at the provider when it is off. Nothing is charged. Journaled.

Access: platform permission platform_admin

Request body

Name Description
provider_id The number provider account (required).
domain_id The domain whose pool receives the numbers; it must be allowed to use the account (required).
numbers Numbers in E.164 format, at most 100 (required).

Response: results: [{number, status (imported, known: already in Telma, not_held: the provider does not list it), phone_id, message (SMS could not be turned on)}], in the order asked.

PUT /api/v1/system/phone-numbers/<phone_id>/voice

Set the voice trunk of a number: the trunk its calls come in through (and go out through when it allows it).

The trunk must be one the number's domain may use (a platform trunk allowed for it, or a trunk of the domain).

Access: platform permission platform_admin

Path parameters

Name Description
phone_id Phone number ID.

Request body

Name Description
voice_trunk_id The trunk; null turns the voice off (required).

Response: phone_number: The number (as in the domain's list).

GET /api/v1/system/providers

The providers of the platform: for each type, its SMS accounts, voice trunks and number provider accounts.

The details of each piece come from GET /system/sms-accounts, /system/voice-trunks and /system/number-providers.

Access: platform permission platform_admin

Response: providers: [{type (one of the types of GET /api/v1/providers, or the type a record names), label, sms_account_ids, voice_trunk_ids, number_provider_ids, numbers (how many numbers use the provider)}].

GET /api/v1/system/providers/<kind>/numbers

The numbers at a provider, with their capabilities.

A number is at the provider when it sends through one of its SMS accounts, or its calls come in through one of its trunks, or it was bought from it.

Access: platform permission platform_admin

Path parameters

Name Description
kind The provider type (as GET /system/providers).

Response: numbers: [{id, number, label, domain_id, domain_name, organization_id, organization_name, is_active, removable, assigned, held, pending_order ({status, placed_at} while the provider has not delivered the number, else null), sms {account_id, account_name, account_provider, mms (the provider sends MMS), mms_configured (the account has its MMS account id)}, voice {enabled, trunk_id, trunk_name, trunk_scope (platform or domain), direction (inbound or both), missing_trunk (voice on without a trunk: the calls come in by the address of the provider's IP trunk)}}], sorted by number.

Platform number porting

Every port request with the provider's reference, the cancellations to do by hand, the follow-up on demand (super admin).

GET /api/v1/system/port-requests

Every port request of the platform, with the provider's account and reference.

Access: platform permission platform_admin

Query parameters

Name Description
state open or a state (as GET /port-requests); cancel_requested lists the cancellations to ask the provider.
domain_id Only this domain.
skip Offset (default 0).
limit Page size (default 50, at most 100).

Response: port_requests: As GET /port-requests, plus provider_reference and account {id, name}. total, skip, limit: The page.

GET /api/v1/system/port-requests/<request_id>

A port request with the provider's events (caption, date and reference of each).

Access: platform permission platform_admin

Path parameters

Name Description
request_id Port request ID.

Response: port_request: As GET /port-requests/<id>, plus provider_reference, account {id, name} and, on each event, caption, provider_at and reference.

POST /api/v1/system/port-requests/<request_id>/cancel-done

The cancellation was done at the provider (by hand): the request is closed (cancelled; completed when some numbers had already arrived).

Access: platform permission platform_admin

Path parameters

Name Description
request_id Port request ID.

Response: port_request: As GET /system/port-requests/<id>.

Errors 409 conflict: no cancellation is waiting.

POST /api/v1/system/port-requests/<request_id>/cancel-refused

The provider could not cancel (too late, already moved): the request goes on, the requester is told why.

Access: platform permission platform_admin

Path parameters

Name Description
request_id Port request ID.

Request body

Name Description
reason What the provider said (up to 300 characters).

Response: port_request: As GET /system/port-requests/<id>.

Errors 409 conflict: no cancellation is waiting.

POST /api/v1/system/port-requests/follow

Read the providers' port events now and put in service the numbers that arrived (the periodic task does the same every 5 minutes).

Access: platform permission platform_admin

Response: result: {: {events, applied, ported, error ({code, message} or null)}}.

Platform voice trunks

SIP trunks configured once by the super admin and shared by the allowed domains; a domain admin only sees their description.

GET /api/v1/system/voice-cost-tables

The cost tables of the platform's providers (python -m app.cli.voice_cost_tables imports them).

Access: platform permission platform_admin

Query parameters

Name Description
archived true to include the tables replaced by a newer import.

Response: voice_cost_tables: [{id, name, currency, scope, effective_at, source, rows, status, replaces, created_at, trunks}] (trunks: the names of the trunks that use the table).

GET /api/v1/system/voice-cost-tables/<table_id>/lookup

The rate a cost table gives a number (longest prefix).

Access: platform permission platform_admin

Path parameters

Name Description
table_id Cost table ID.

Query parameters

Name Description
number The number, E.164 or with its international prefix (required).

Response: rate: {prefix, description, rate_per_minute, connection_fee} or null when no prefix covers the number. currency: The table's currency.

GET /api/v1/system/voice-pricing

The platform's margin on the voice calls (the price billed to the domains), and the USD to CAD rate.

Access: platform permission platform_admin

Response: voice_pricing: {markup_percent, min_markup_per_minute, min_billed_seconds, increment_seconds, connection_fee, usd_to_cad}.

PUT /api/v1/system/voice-pricing

Change the platform's margin (the fields given).

Access: platform permission platform_admin

Request body

Name Description
markup_percent Margin on the cost, 0 to 1000 %.
min_markup_per_minute The margin per minute is at least this (CAD), 0 to 100.
min_billed_seconds A call that connects bills at least this, 0 to 600 seconds.
increment_seconds Billed in blocks of this, 1 to 600 seconds (1: by the second; 6, 30, 60...).
connection_fee Per call that connects (CAD), 0 to 100.
usd_to_cad Rate applied to a cost table in USD, 0.5 to 3.

Response: voice_pricing: As GET.

GET /api/v1/system/voice-trunks

List the platform voice trunks.

Access: platform permission platform_admin

Response: voice_trunks: The platform trunks with their credentials (password masked), numbers (how many numbers name each) and domains (the domains allowed to use it, [{id, name}]).

POST /api/v1/system/voice-trunks

Create a platform voice trunk.

Access: platform permission platform_admin
API keys: refused, 403 forbidden (API key not allowed on this route)

Request body

Name Description
name Trunk name (unique), 1 to 64 characters, no character that would change a command of the voice server (see POST /domains/<id>/voice-trunks); never shown to a domain admin.
description What the domain admins of the allowed domains see of this trunk (up to 200 characters), in place of its name and host.
host SIP server hostname or IP address.
username SIP username.
password SIP password.
provider Name of the provider (free text, for the administrators).
port SIP port (default: 5060).
server_id Voice server that registers this trunk; default: the least loaded enabled server.
trunk_type "registration" (default) or "ip_auth" (the provider identifies us by IP).
outbound_enabled Calls may go out through it (default: yes for registration, no for ip_auth).
inbound_enabled Calls may come in through it (default: yes; false: outbound only; not both false).
outbound_trunk_id A trunk without outbound: another trunk of the same owner, open to outbound, that its outbound calls and forwards take instead (empty: none).
max_channels Simultaneous calls it carries, inbound and outbound together (empty: no limit).
allowed_ips ip_auth only: other IP addresses the carrier calls from (list, or one per line).
cost_table_id The platform cost table of the provider (GET /system/voice-cost-tables): what a call through the trunk costs us (empty: none).

Response: voice_trunk: The created trunk, with its credentials and cost_table_id, cost_table_name.

DELETE /api/v1/system/voice-trunks/<trunk_id>

Delete a platform trunk that no domain and no number uses.

Access: platform permission platform_admin
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
trunk_id Trunk ID.

Response: message: Confirmation.

Errors 409 conflict: domains are allowed to use it or numbers name it (numbers, domains in the answer).

PUT /api/v1/system/voice-trunks/<trunk_id>

Update a platform voice trunk.

Access: platform permission platform_admin
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
trunk_id Trunk ID.

Request body

Name Description
name Trunk name.
description What the domain admins see of the trunk.
host SIP server hostname or address.
username SIP username.
password SIP password (empty or "**" keeps it).
port SIP port.
enabled Whether the trunk is enabled.
server_id Voice server that registers this trunk.
outbound_enabled Calls may go out through it.
inbound_enabled Calls may come in through it (default: yes; false: outbound only; not both false).
outbound_trunk_id A trunk without outbound: another trunk of the same owner, open to outbound, that its outbound calls and forwards take instead (empty: none).
max_channels Simultaneous calls it carries (empty: no limit).
allowed_ips ip_auth only: other IP addresses the carrier calls from.
cost_table_id The platform cost table of the provider (empty: none).
static_config false hands a trunk still marked as defined in the voice server's static configuration to the database (every new trunk is generated from the database; true is ignored).

Response: voice_trunk: The updated trunk.

PUT /api/v1/system/voice-trunks/<trunk_id>/domains

Set the domains allowed to use a platform trunk (the same allowance as PUT /domains/<id>/platform-voice-trunks, seen from the trunk).

Access: platform permission platform_admin
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
trunk_id Trunk ID.

Request body

Name Description
domain_ids Ids of the domains that may use the trunk (the whole list; [] = none).

Response: voice_trunk: The trunk with its domains.

Errors 400 validation_error: an id is not a domain. 409 conflict: a domain taken away still has numbers on the trunk (domain_id, numbers in the answer).

GET /api/v1/system/voice-trunks/status

State of the platform trunks, from the voice server.

Access: platform permission platform_admin

Response: registrations: State of each platform trunk, by trunk name: a registration trunk reads Registered, Rejected, Unregistered...; an IP trunk (it does not register) Reachable or Unreachable, from the regular checks of the voice server, or Unknown before the first answer.

Profiles and permissions

Catalogue of the permissions, profiles of the organization and of the domain, profiles of a member, platform permissions of an account.

GET /api/v1/domains/<domain_id>/profiles

The profiles a domain offers to its organizations (copies of the built-in ones the reseller adjusts).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Response: profiles: [{id, name, permissions, domain_id}]. builtin_profiles: The built-in profiles, to copy.

POST /api/v1/domains/<domain_id>/profiles

Create a profile of the domain, offered to all its organizations.

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.

Request body

Name Description
name 1 to 60 characters (required).
permissions Permission names of GET /api/v1/permissions (required unless copy_of is given).
copy_of A built-in profile (builtin:admin, builtin:user) or another profile of the domain to start from.

Response: profile: The created profile.

DELETE /api/v1/domains/<domain_id>/profiles/<profile_id>

Delete a profile of the domain; refused while members hold it (409 profile_in_use).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.
profile_id Profile ID.

Response: success: True.

PUT /api/v1/domains/<domain_id>/profiles/<profile_id>

Rename a profile of the domain or change its permissions (every member holding it in the domain's organizations gets the change).

Access: platform permission platform_admin or domain_admin (domain_admin: only its domains, domain_id)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
domain_id Domain ID.
profile_id Profile ID.

Request body

Name Description
name 1 to 60 characters (optional).
permissions Permission names of GET /api/v1/permissions, replacing the list (optional).

Response: profile: The profile.

Errors 402 plan_limit: in one of the organizations where members hold the profile, the new permissions would give the messages (messages.read or messages.send) to more members than its messaging plan allows; nothing is changed, in any organization. The answer names them: organizations [{id, name, error (plan_limit or last_admin), limit, plan, upgrade, current}]. 409 last_admin: an organization would have nobody left with members.admin (and none goes beyond its plan); organizations as above.

PUT /api/v1/organizations/current/members/<user_id>/profiles

Give profiles to a member of the current organization (they add up: the member holds every permission of each).

The list replaces the member's profiles. The member's real-time rooms follow at once. 404 not_found when the user is not a member of the current organization; 400 validation_error (field profile_ids) for an empty list or a profile the organization cannot give.

Access: permission members.admin in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
user_id User ID of the member.

Request body

Name Description
profile_ids One or more ids of GET /api/v1/organizations/current/profiles (required).

Response: member: {user_id, profile_ids, role (the former role it amounts to: admin, manager or agent; a copy), permissions}.

Errors 403 forbidden: the member holds a permission the caller does not hold in the organization, or the profiles give one (field profile_ids). 409 last_admin: the organization would have nobody left who holds members.admin. 402 plan_limit: the member would start using the messages (messages.read or messages.send) beyond max_users of the organization's messaging plan: {error, message, field (profile_ids), limit (users), plan, upgrade (the first plan with room, or null), current (members who use the messages and pending invitations)}, as everywhere else.

GET /api/v1/organizations/current/profiles

The profiles members of the current organization can be given: the built-in ones (Admin, User), those of its domain, its own.

404 no_organization without a current organization.

Access: permission members.manage or members.admin in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Response: profiles: [{id, key, name, permissions, builtin, organization_id, domain_id (profiles of the domain and of the organization), members (members of the organization who hold it)}], the built-in ones first, then those of the domain, then the organization's own.

POST /api/v1/organizations/current/profiles

Create a profile of the current organization.

404 no_organization without a current organization; 400 validation_error (field name, permissions or copy_of) for a bad name, an unknown permission or an unknown profile to copy.

Access: permission members.admin in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Request body

Name Description
name 1 to 60 characters (required).
permissions Permission names of the organization (GET /api/v1/permissions); required unless copy_of is given.
copy_of Id of a profile to start from (builtin:admin, builtin:user, one of the domain or of the organization): its permissions when permissions is absent.

Response: profile: The created profile.

DELETE /api/v1/organizations/current/profiles/<profile_id>

Delete a profile of the current organization; refused while members hold it (409 profile_in_use).

Access: permission members.admin in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
profile_id Profile ID.

Response: success: True.

PUT /api/v1/organizations/current/profiles/<profile_id>

Rename a profile of the current organization or change its permissions (the built-in ones and those of the domain cannot change here: 404).

Access: permission members.admin in the organization (profiles)
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
profile_id Profile ID.

Request body

Name Description
name 1 to 60 characters (optional).
permissions Permission names of GET /api/v1/permissions, replacing the list (optional).

Response: profile: The profile. The members who hold it get its new permissions at once.

Errors 402 plan_limit: the new permissions give the messages (messages.read or messages.send) to members who hold the profile and did not have them, beyond max_users of the organization's messaging plan: {error, message, field (permissions), limit (users), plan, upgrade, current}, as everywhere else; nothing is changed. 409 last_admin: the organization would have nobody left with members.admin; nothing is changed.

GET /api/v1/permissions

The catalogue of permissions.

Any authenticated session; closed to API keys.

Access: authenticated (JWT or API key)
API keys: refused, 403 forbidden (API key not allowed on this route)

Response: organization: The permissions of an organization a profile can hold, in the order of the screens: [{name, description}]. platform: The permissions of the platform an account can hold: platform_admin (every domain and organization), domain_admin (the domains of the account), support. builtin_profiles: The built-in profiles of every organization, never changed: [{id, key, name, permissions}].

PUT /api/v1/users/<user_id>/platform-permissions

The platform permissions of an account (platform_admin only). The sessions of the account end (its rights changed).

Never on one's own account (403 forbidden). The stored role follows (super_admin, domain_admin, or the role of its current organization's profiles). support is the grant of PUT /api/v1/users/<id> (permissions).

Access: platform permission platform_admin
API keys: refused, 403 forbidden (API key not allowed on this route)

Path parameters

Name Description
user_id User ID.

Request body

Name Description
permissions Any of platform_admin, domain_admin, support (required; [] removes them; the list replaces the current one).
domain_ids The domains a domain_admin administers (optional; absent keeps them).

Response: user: {id, platform_permissions (support included when granted), domain_ids}.

Uploads

MMS attachments.

GET /api/v1/media-proxy

Proxy and convert remote media files (public, no authentication).

Serves the media of inbound MMS, which live on the SMS provider's servers: browsers load them in image, audio and video elements or open them from a link, and webhook payloads give them as proxied_url. AMR audio is converted to MP3 and 3GPP video to MP4, which browsers cannot play otherwise.

The answer is the file itself, always with X-Content-Type-Options: nosniff. Images, audio and video (image/..., audio/..., video/..., SVG excluded), PDF (application/pdf) and plain text (text/plain, with the charset the provider gives, else utf-8) keep the Content-Type of the fetched file (audio/mpeg or video/mp4 once converted), with Content-Disposition: inline. Any other file is a download: application/octet-stream with Content-Disposition: attachment; filename="media.<ext>", where <ext> is the extension of the fetched type (vcf for a vCard, bin when the type is unknown or missing).

At most 4 files are fetched and 1 converted at once: a request waits its turn, in order of arrival (15 s at most for a fetch, behind 100 others at most; 30 s for a conversion, behind 8 others), else it gets 503 busy at once. A converted file is kept in a cache named after the content fetched (SHA-256), looked up before ffmpeg runs: the same media asked under several URLs is converted once. A file fetched is also kept as is, up to 10 minutes after its fetch (100 MB and 1000 files in all, the oldest go first): the same URL asked again meanwhile, by anyone and whatever its content_type, is served from there (converted or not) without fetching it again, and a request for the same URL while it is being fetched waits for that fetch. Files served from the disk accept Range requests (206 Partial Content).

Access: public (no authentication)
Rate limit: exempt from the general per-address limit

Query parameters

Name Description
url Media URL to fetch (required): http:// or https:// (https:// is assumed when the scheme is missing), on port 80 or 443 only, without credentials (nothing before an @), backslash, blank or control character; the host must be a host name (not an IP address) among the SMS providers' media hosts (MEDIA_PROXY_ALLOWED_HOSTS, and their subdomains), and all its addresses public, checked again when connecting (the connection goes to one of these checked addresses). The fragment is dropped. Redirects are not followed; 5 MB max (MEDIA_PROXY_MAX_BYTES), 90 s for the whole fetch. 400 validation_error when missing or not allowed.
content_type Original content type of the media (optional), only used to ask for a conversion: audio/amr (and audio/amr-wb) converts to MP3, video/3gpp (and video/3gpp2) to MP4, as does an URL ending in .amr, .3gp or .3gpp. The file is converted only when it is AMR / 3GPP by the type the provider gives it or by its first bytes (the AMR header, the 3gp / 3g2 brand of a 3GPP file); otherwise it is served unconverted, and a failed conversion serves the original, as the type of the fetched file says. It never sets the Content-Type of the answer.

Response: The media content, possibly converted. Errors: 404 not_found (no message carries this media URL: pass url exactly as in the message's media_urls[].url), 400 validation_error (url missing or not allowed, including a host that resolves to an address that is not public when the connection is made), 502 fetch_failed (the provider did not answer with the file in time, answered with a redirect, or the file is over 5 MB), 503 busy (too many fetches or conversions in progress: retry a few seconds later). Every error is JSON {error, message}.

POST /api/v1/upload

Upload a file for an MMS attachment.

Send the file as multipart/form-data, then pass the returned url in media_urls of POST /api/v1/sms/send (the only attachments it accepts).

Access: authenticated (JWT or API key)

Request body

Name Description
file The file (multipart/form-data field, required): jpg, jpeg, png, gif, webp, mp4, mov, mp3, wav or m4a (by file name extension), 10 MB max. 400 validation_error when the field is missing or empty, the extension is not accepted or the file is larger.

Response: url: Public URL of the file, to pass as is in media_urls of POST /api/v1/sms/send. content_type: MIME type deduced from the extension. filename: Stored file name (201).

GET /api/v1/uploads/<filename>

Serve an uploaded file.

Access: public (no authentication)
Rate limit: exempt from the general per-address limit

Path parameters

Name Description
filename Name of the uploaded file.

Response: The file content.

Inbound webhooks

URLs called by SMS providers to deliver inbound messages.

GET /webhook/domain/<provider>/<secret_token>

Receive incoming SMS for a domain.

This is the secret webhook URL for domain-level SMS reception. Each provider has its own URL path. Never rate limited: the secret token of the URL authenticates the provider.

Access: public (no authentication)
Rate limit: exempt from the general per-address limit

Path parameters

Name Description
provider SMS provider type (one of GET /api/v1/providers).
secret_token Domain's webhook secret token.

Response: success: true. domain_id: The domain. phone_number: The number that received the message. routed_to_team: Whether a team of the organization that owns the number received it. message_id: The recorded message. duplicate: true when the provider message id was already recorded: nothing is recorded again. notes: Remarks on the routing, one string each.

Errors These routes answer providers, not integrators: their errors are {"error": "<free text>"} without message or stable code. 400 "Invalid provider" (with the name). 404 "Invalid webhook token" or "Phone number not found in domain". 401 "Invalid signature".

POST /webhook/domain/<provider>/<secret_token>

Receive incoming SMS for a domain.

This is the secret webhook URL for domain-level SMS reception. Each provider has its own URL path. Never rate limited: the secret token of the URL authenticates the provider.

Access: public (no authentication)
Rate limit: exempt from the general per-address limit

Path parameters

Name Description
provider SMS provider type (one of GET /api/v1/providers).
secret_token Domain's webhook secret token.

Response: success: true. domain_id: The domain. phone_number: The number that received the message. routed_to_team: Whether a team of the organization that owns the number received it. message_id: The recorded message. duplicate: true when the provider message id was already recorded: nothing is recorded again. notes: Remarks on the routing, one string each.

Errors These routes answer providers, not integrators: their errors are {"error": "<free text>"} without message or stable code. 400 "Invalid provider" (with the name). 404 "Invalid webhook token" or "Phone number not found in domain". 401 "Invalid signature".

GET /webhook/provider/<provider>/<secret_token>

Receive incoming SMS/MMS from a provider.

This endpoint looks up the provider webhook by secret token. The phone number is then used to route to the appropriate team. Never rate limited: the secret token of the URL authenticates the provider.

Access: public (no authentication)
Rate limit: exempt from the general per-address limit

Path parameters

Name Description
provider SMS provider type (one of GET /api/v1/providers).
secret_token Provider webhook secret token.

Response: For GET: "Ready!" response. For POST: "ok" for SMS, HTTP 200 for MMS.

Errors These routes answer providers, not integrators: their errors are {"error": "<free text>"} without message or stable code. 404 "Invalid provider or webhook token" or "Phone number not found". 400 "Missing destination phone number".

POST /webhook/provider/<provider>/<secret_token>

Receive incoming SMS/MMS from a provider.

This endpoint looks up the provider webhook by secret token. The phone number is then used to route to the appropriate team. Never rate limited: the secret token of the URL authenticates the provider.

Access: public (no authentication)
Rate limit: exempt from the general per-address limit

Path parameters

Name Description
provider SMS provider type (one of GET /api/v1/providers).
secret_token Provider webhook secret token.

Response: For GET: "Ready!" response. For POST: "ok" for SMS, HTTP 200 for MMS.

Errors These routes answer providers, not integrators: their errors are {"error": "<free text>"} without message or stable code. 404 "Invalid provider or webhook token" or "Phone number not found". 400 "Missing destination phone number".

GET /webhook/sms/<secret_token>

Receive incoming SMS from provider.

This is the secret webhook URL that providers call when an SMS is received. The secret_token ensures only legitimate requests are processed. Never rate limited: the secret token of the URL authenticates the provider.

A closed or archived conversation is reopened by the message, once it is recorded (like the two other inbound routes): conversation.updated with change "reopened", cause "inbound" and the message_id of that message, the conversation already counting it (last_message_at, unread_count). A duplicate delivery (same provider message id) reopens nothing.

Access: public (no authentication)
Rate limit: exempt from the general per-address limit

Path parameters

Name Description
secret_token Team's webhook secret token.

Response: success: true. message_id: The recorded message (absent on a duplicate delivery). duplicate: true when the provider message id was already recorded: nothing is recorded again. within_hours: Whether the message arrived within the business hours of the team. queued: Whether it was held for the opening of the team (outside the business hours). auto_reply_sent: Whether the automatic reply was sent.

Errors These routes answer providers, not integrators: their errors are {"error": "<free text>"} without message or stable code. 404 "Invalid webhook token". 400 "No provider configured". 401 "Invalid signature".

POST /webhook/sms/<secret_token>

Receive incoming SMS from provider.

This is the secret webhook URL that providers call when an SMS is received. The secret_token ensures only legitimate requests are processed. Never rate limited: the secret token of the URL authenticates the provider.

A closed or archived conversation is reopened by the message, once it is recorded (like the two other inbound routes): conversation.updated with change "reopened", cause "inbound" and the message_id of that message, the conversation already counting it (last_message_at, unread_count). A duplicate delivery (same provider message id) reopens nothing.

Access: public (no authentication)
Rate limit: exempt from the general per-address limit

Path parameters

Name Description
secret_token Team's webhook secret token.

Response: success: true. message_id: The recorded message (absent on a duplicate delivery). duplicate: true when the provider message id was already recorded: nothing is recorded again. within_hours: Whether the message arrived within the business hours of the team. queued: Whether it was held for the opening of the team (outside the business hours). auto_reply_sent: Whether the automatic reply was sent.

Errors These routes answer providers, not integrators: their errors are {"error": "<free text>"} without message or stable code. 404 "Invalid webhook token". 400 "No provider configured". 401 "Invalid signature".

POST /webhook/sms/<secret_token>/status

Update message delivery status from provider.

Never rate limited: the secret token of the URL authenticates the provider.

Access: public (no authentication)
Rate limit: exempt from the general per-address limit

Path parameters

Name Description
secret_token Team's webhook secret token.

Response: Acknowledgment.

Errors These routes answer providers, not integrators: their errors are {"error": "<free text>"} without message or stable code. 404 "Invalid webhook token" or "Message not found". 400 "Missing message ID" or "Unknown provider".

API keys: closed routes

This section is generated from backend/app/utils/permissions.py and from the routes (it is not written by hand). An API key (X-API-Key header) does not open everything a session (JWT) opens: the routes that would give another way to sign in, that spend the organization's money or that administer the account are closed to it. The refusal is a 403 {"error": "forbidden", "message": "API key not allowed on this route"}.

Closed to every API key

On these routes a request that carries an X-API-Key header gets 403 as soon as the header is present, whether the key is ordinary, restricted or even invalid:

  • Second factor and passkeys: the 13 operations of the group
  • Organizations: POST /api/v1/organizations/<org_id>/members/<user_id>/impersonate
  • Invitations: POST /api/v1/invitations, POST /api/v1/invitations/<invitation_id>/resend, POST /api/v1/invitations/accept
  • Users: POST /api/v1/users
  • Integrations: the 9 operations of the group
  • Domain voice settings: PUT /api/v1/domains/<domain_id>/organizations/<organization_id>/voice-outbound
  • Voice (internal): the 3 operations of the group
  • Billing: POST /api/v1/billing/admin/add-credits
  • Free trial, account state and settings: PUT /api/v1/domains/<domain_id>/account-settings
  • Domain billing: the 19 operations of the group
  • Domains: POST /api/v1/domains/<domain_id>/admins, DELETE /api/v1/domains/<domain_id>/admins/<user_id>, POST /api/v1/domains/<domain_id>/organizations/<organization_id>/credit, PUT /api/v1/domains/<domain_id>/platform-voice-trunks, PUT /api/v1/domains/<domain_id>/security-settings, PUT /api/v1/domains/<domain_id>/settings/default-payment-provider, PUT /api/v1/domains/<domain_id>/settings/oauth/<provider>, PUT /api/v1/domains/<domain_id>/settings/payment-providers/<provider>, PUT /api/v1/domains/<domain_id>/settings/smtp, POST /api/v1/domains/<domain_id>/settings/smtp/test, PUT /api/v1/domains/<domain_id>/sms-providers/<provider>, POST /api/v1/domains/<domain_id>/voice-trunks, DELETE /api/v1/domains/<domain_id>/voice-trunks/<trunk_id>, PUT /api/v1/domains/<domain_id>/voice-trunks/<trunk_id>, POST /api/v1/domains/<domain_id>/webhook/regenerate
  • System: PUT /api/v1/system/oauth/<provider>, PUT /api/v1/system/smtp
  • Platform SMS provider accounts: POST /api/v1/system/sms-accounts, DELETE /api/v1/system/sms-accounts/<account_id>, PUT /api/v1/system/sms-accounts/<account_id>, PUT /api/v1/system/sms-accounts/<account_id>/domains, POST /api/v1/system/sms-accounts/<account_id>/numbers, DELETE /api/v1/system/sms-accounts/<account_id>/numbers/<phone_id>, POST /api/v1/system/sms-accounts/<account_id>/numbers/move, POST /api/v1/system/sms-accounts/<account_id>/regenerate-secret
  • SMS provider accounts of a domain: PUT /api/v1/domains/<domain_id>/platform-sms-accounts, PUT /api/v1/domains/<domain_id>/sms-account-settings, POST /api/v1/domains/<domain_id>/sms-accounts, DELETE /api/v1/domains/<domain_id>/sms-accounts/<account_id>, PUT /api/v1/domains/<domain_id>/sms-accounts/<account_id>, POST /api/v1/domains/<domain_id>/sms-accounts/<account_id>/numbers, DELETE /api/v1/domains/<domain_id>/sms-accounts/<account_id>/numbers/<phone_id>, POST /api/v1/domains/<domain_id>/sms-accounts/<account_id>/regenerate-secret
  • Platform voice trunks: POST /api/v1/system/voice-trunks, DELETE /api/v1/system/voice-trunks/<trunk_id>, PUT /api/v1/system/voice-trunks/<trunk_id>, PUT /api/v1/system/voice-trunks/<trunk_id>/domains
  • Profiles and permissions: the 11 operations of the group

On these, a valid key, ordinary or restricted, gets 403; an unknown or revoked key gets 401 as anywhere else:

  • Phone numbers: PATCH /api/v1/phone-numbers/<phone_id>, GET /api/v1/phone-numbers/<phone_id>/prices, POST /api/v1/phone-numbers/<phone_id>/release, POST /api/v1/phone-numbers/<phone_id>/restore
  • Number purchase: the 4 operations of the group
  • API keys: POST /api/v1/keys
  • Voice (VoXoS): GET /api/v1/voice/access-numbers, GET /api/v1/voice/call-info, PUT /api/v1/voice/call-info/recording, PUT /api/v1/voice/call-info/recording/skip-next, POST /api/v1/voice/call-requests, DELETE /api/v1/voice/call-requests/<request_id>, GET /api/v1/voice/call-requests/<request_id>, POST /api/v1/voice/diagnostics, GET /api/v1/voice/directory, GET /api/v1/voice/my-endpoints, PUT /api/v1/voice/my-endpoints/<endpoint_id>/settings

Closed to restricted keys

A restricted key (limited to a team, or the key of an integration) acts as an agent of the teams it reaches and never reads organization-wide data. It gets 403 on everything below; an ordinary key has the rights of its creator.

Closed according to the body

On these routes an API key gets 403 as soon as the request body does what follows (the same call stays open to a session).

  • PUT /api/v1/domains/<domain_id>: refused when the body contains hostnames, name
  • PUT /api/v1/organizations/<org_id>/members/<user_id>: refused when the body gives the role admin, domain_admin, super_admin
  • PUT /api/v1/users/<user_id>: refused when the body gives the role admin, domain_admin, super_admin; refused when the body contains password
  • PUT /api/v1/users/<user_id>/role: refused when the body gives the role admin, domain_admin, super_admin