Routes
Every route the Ostra API and the MCP server serve, in one fixed form: the credential it takes, the scopes or role it needs, its Idempotency-Key, what it answers and its notable refusals. This list is read from the server’s own route table when these pages are built, so a route cannot be missing from it or listed here without existing.
Paths are under the base URL. Refusals link to the error codes; any route can also answer 400 invalid_request, 401 unauthenticated, 403 forbidden or insufficient_scope, 429 rate_limited and 500 internal_error, and a line or item you may not see is always 404 not_found. An answer written {a, b} is an object with those fields; a name, such as Line, is that object itself, as GET /v1/lines/{line_id} gives it.
Lines
Your lines and the sandbox line: status, addresses, proxy endpoints and credentials, a new carrier IP, the connection record and usage.
- GET
/v1/lines - GET
/v1/lines/{line_id} - GET
/v1/lines/{line_id}/ip - POST
/v1/lines/{line_id}/new-carrier-ip - GET
/v1/lines/{line_id}/new-carrier-ip/{job_id} - GET
/v1/lines/{line_id}/proxy - POST
/v1/lines/{line_id}/proxy-credentials - DELETE
/v1/lines/{line_id}/proxy-credentials/{credential_id} - GET
/v1/lines/{line_id}/sources - PUT
/v1/lines/{line_id}/sources - GET
/v1/lines/{line_id}/attestations - POST
/v1/lines/{line_id}/attestations - GET
/v1/lines/{line_id}/connections - GET
/v1/lines/{line_id}/connections/export - GET
/v1/lines/{line_id}/report - GET
/v1/usage
GET /v1/lines
The lines your organization holds now, then the sandbox line.
- Credential
- API key, OAuth token or session
- Scopes
lines:read- Answers
200{lines: [Line]}
GET /v1/lines/{line_id}
One line: status, carrier, number, addresses, health and proxy ports.
- Credential
- API key, OAuth token or session
- Scopes
lines:read- Answers
200Line- Refusals
not_found
GET /v1/lines/{line_id}/ip
The exit addresses the server's own checks last saw, with their networks.
- Credential
- API key, OAuth token or session
- Scopes
lines:read- Answers
200{line_id, sandbox, ipv4, ipv6, asn4, asn6, checked_at, geolocation}. geolocation is null on a real line; the sandbox gives a labelled sample.
POST /v1/lines/{line_id}/new-carrier-ip
Ask the carrier for a new IP address on this line. Returns at once with a job to read.
- Credential
- API key, OAuth token or session
- Scopes
lines:new_carrier_ip- Idempotency-Key
- Required
- Request
- No body, or
{}. - Answers
202{job}; the same answer again, with Ostra-Idempotent-Replay: true, for a repeated Idempotency-Key.
GET /v1/lines/{line_id}/new-carrier-ip/{job_id}
A new carrier IP request's state: pending, running, succeeded, failed, expired or refused.
- Credential
- API key, OAuth token or session
- Scopes
lines:new_carrier_ip- Answers
200{job}- Refusals
not_found
GET /v1/lines/{line_id}/proxy
The line's proxy endpoints, whether the gateway is taking credentials, and the line's credentials without their secrets.
- Credential
- API key, OAuth token or session
- Scopes
lines:read- Answers
200{line_id, sandbox, endpoints, gateway, credentials}
POST /v1/lines/{line_id}/proxy-credentials
Issue a username and secret for the line's HTTPS proxy port. The secret is in this answer only.
- Credential
- API key, OAuth token or session
- Scopes
proxy_credentials:write- Idempotency-Key
- Accepted
- Request
{label?, expires_in_seconds?}(60 to 31536000; none means no expiry).- Answers
201{credential, secret}- Refusals
gateway_pendingline_suspendedconflict
DELETE /v1/lines/{line_id}/proxy-credentials/{credential_id}
Revoke one credential. New sign-ins with it stop; a tunnel it already opened runs until it closes.
- Credential
- API key, OAuth token or session
- Scopes
proxy_credentials:write- Answers
204- Refusals
gateway_pendingnot_found
GET /v1/lines/{line_id}/sources
The source addresses the line's SOCKS5 and HTTP ports answer, with whether the gateway has each one. No list, no answer.
- Credential
- API key, OAuth token or session
- Scopes
lines:read- Answers
200{line_id, sandbox, ports, gateway, sources, removing, limits}- Refusals
not_found
PUT /v1/lines/{line_id}/sources
Set the source addresses the line's SOCKS5 and HTTP ports answer: the whole list, replacing the one there. Public addresses and networks no wider than a /24 or /48, at most 20.
- Credential
- API key, OAuth token or session
- Scopes
proxy_credentials:write- Request
{sources: [{address, label?}]}; [] empties it.- Answers
200as GET
GET /v1/lines/{line_id}/attestations
The line's stated purpose (its one basis, with who stated it and when), every earlier statement, and its blocked-site exceptions.
- Credential
- API key, OAuth token or session
- Scopes
lines:read- Answers
200{line_id, sandbox, current, attestations, exceptions}- Refusals
not_found
POST /v1/lines/{line_id}/attestations
State the line's purpose again: own_service, permission_to_test or line_number. It supersedes the current statement; a service under a blocked site asks Ostra to open it for this line (never ticketing).
- Credential
- API key, OAuth token or session
- Scopes
lines:attest- Idempotency-Key
- Accepted
- Request
{basis, services?, operator_name?, permission_reference?}- Answers
201{attestation, exceptions}- Refusals
invalid_requestforbiddennot_found
GET /v1/lines/{line_id}/connections
The connection record for your own time on the line: when, from where, to which host and port, how many bytes, and whether Ostra refused it. Never content.
- Credential
- API key, OAuth token or session
- Scopes
connections:read- Request
?from=&to= (ISO 8601; default the last 24 hours; at most 90 days) &limit= (1 to 5000, default 1000).- Answers
200{line_id, sandbox, from, to, connections, totals}
GET /v1/lines/{line_id}/connections/export
Every connection record in a range as one file to save: CSV or JSON, streamed, with no limit on rows.
- Credential
- API key, OAuth token or session
- Scopes
connections:read- Request
?from=&to= (ISO 8601; default the last 24 hours; at most 90 days) &format=csv|json (default csv).- Answers
200the file, Content-Disposition: attachment
GET /v1/lines/{line_id}/report
The line report: on a real line it answers available false, since line reports are not kept on the server; the sandbox gives a fixed, labelled sample.
- Credential
- API key, OAuth token or session
- Scopes
reports:read- Answers
200{line_id, available, ...}
GET /v1/usage
Connections and bytes in and out per line per day, for the lines while your organization held them.
- Credential
- API key, OAuth token or session
- Scopes
usage:read- Request
?from=&to= (YYYY-MM-DD, UTC; at most 92 days; default the last 30).- Answers
200{from, to, days}
Orders
Order lines for your organization. Available lines are assigned at once; the rest wait in a first-come queue, and nothing is charged until a line is assigned. A signed-in session also needs the admin role to order.
- GET
/v1/orders - GET
/v1/orders/renewal-terms - POST
/v1/orders/renewal-terms - POST
/v1/orders - GET
/v1/orders/{order_id} - POST
/v1/orders/{order_id}/cancel - POST
/v1/orders/{order_id}/resume
GET /v1/orders
Your organization's line orders, open ones first, then the newest, 50 at most.
- Credential
- API key, OAuth token or session
- Scopes
orders:read- Answers
200{orders: [Order]}
GET /v1/orders/renewal-terms
The price and renewal terms an order agrees to, word for word as the dashboard shows them beside its consent: each line renews every month until you cancel it, its price, the calls charge (5 cents a minute, which varies with your calls), no minimum, any announced price change, and how to cancel online. Show them to the person agreeing.
- Credential
- API key, OAuth token or session
- Scopes
orders:read- Answers
200{required, renewal_terms: RenewalTerms | null}: required is true while billing is on; renewal_terms is null while the line has no price
POST /v1/orders/renewal-terms
Agree to the renewal terms in force with no order: for an organization holding lines it never agreed these terms for, or after a price changed. Kept for good, as every agreement is. A person agrees, signed in to the dashboard or an Ostra app: an API key is refused (forbidden).
- Credential
- OAuth token or session: a person, never an API key
- Scopes
orders:write- Request
{accept_renewal_terms}: the id of the renewal terms in force.- Answers
201{terms_id, agreed_at}
POST /v1/orders
Order lines. Available lines are assigned at once, each charged its first month when it is assigned; the rest wait in a first-come queue with nothing charged, and you get an email as each line is ready. An order that takes your organization above 5 lines is checked by a person at Ostra first.
- Credential
- API key, OAuth token or session
- Scopes
orders:write- Idempotency-Key
- Accepted
- Request
{quantity, carrier?, attestation, accept_renewal_terms?}: quantity 1 to 1000; carrier att, verizon or tmobile for lines on that carrier only, or left out for any carrier; attestation is every line's purpose,{basis, services?, operator_name?, permission_reference?}; accept_renewal_terms the id of the renewal terms in force (GET /v1/orders/renewal-terms), your agreement to them, needed while billing is on. An API key names none (it cannot agree: forbidden) and orders under the terms a person of the organization last agreed to.- Answers
201Order
GET /v1/orders/{order_id}
One order: how many lines, which are assigned, what it still waits for, and a sentence saying where it stands.
- Credential
- API key, OAuth token or session
- Scopes
orders:read- Answers
200Order- Refusals
not_found
POST /v1/orders/{order_id}/cancel
Cancel an order not yet filled. Lines already assigned from it stay yours.
- Credential
- API key, OAuth token or session
- Scopes
orders:write- Request
{}- Answers
200Order
POST /v1/orders/{order_id}/resume
Resume a paused order (a declined card, a payment owed, renewal terms to agree to, or an order from before orders stated their lines' purpose, which states it now). It keeps its place in the queue and is tried at once.
- Credential
- API key, OAuth token or session
- Scopes
orders:write- Request
{accept_renewal_terms?, attestation?}: the id of the renewal terms in force, when your organization has not agreed to them (an order paused consent_needed). attestation is the lines' purpose,{basis, services?, operator_name?, permission_reference?}, needed for an order paused purpose_needed.- Answers
200Order
Texts and calls
What reaches a line's number. A list never carries a body unless you ask for content, and every body or code returned is written to Ostra's audit log.
- GET
/v1/lines/{line_id}/messages - GET
/v1/lines/{line_id}/messages/wait - GET
/v1/lines/{line_id}/messages/{message_id} - POST
/v1/lines/{line_id}/simulate/message - GET
/v1/lines/{line_id}/calls - GET
/v1/lines/{line_id}/calls/{call_id} - GET
/v1/labels - PUT
/v1/labels/{number} - DELETE
/v1/labels/{number}
GET /v1/lines/{line_id}/messages
Texts received while your organization held the line, oldest first, by cursor.
- Credential
- API key, OAuth token or session
- Scopes
messages:read, plusmessages:read_contentwith include_content=true- Request
?cursor=&limit= (1 to200, default 50) &include_content= (true or false).- Answers
200{line_id, messages, next_cursor}
GET /v1/lines/{line_id}/messages/wait
Wait for the first text after a cursor (no cursor: the next text from now).
- Credential
- API key, OAuth token or session
- Scopes
messages:read, plusmessages:read_contentwith include_content=true- Request
?cursor=&timeout= (1 to 90 seconds, default 60) &limit= (1 to 50) &include_content=.- Answers
200as the list when a text arrives;204with Ostra-Cursor on timeout.- Refusals
rate_limited
GET /v1/lines/{line_id}/messages/{message_id}
One text with its body and code. The view is written to the audit log before anything is decrypted.
- Credential
- API key, OAuth token or session
- Scopes
messages:readandmessages:read_content- Answers
200{message}- Refusals
not_found
POST /v1/lines/{line_id}/simulate/message
Sandbox line only: store a simulated text, and fire message.received when the credential also holds webhooks:write.
- Credential
- API key, OAuth token or session
- Scopes
messages:read- Request
{sender?, body?}(body at most 1000 characters; defaults: +13105550100 and a text with a six-digit code).- Answers
201{message, webhook_event}: webhook_event says whether message.received was queued- Refusals
sandbox_onlyrate_limited
GET /v1/lines/{line_id}/calls
Calls to the line, oldest first, by cursor: who called, when, how long, how each ended (in_progress, answered, missed, voicemail or rejected), and Twilio's check of the caller ID (stir_verstat, or null).
- Credential
- API key, OAuth token or session
- Scopes
calls:read, pluscalls:read_contentwith include_content=true- Request
?cursor=&limit=&include_content=.- Answers
200{line_id, calls, next_cursor}
GET /v1/lines/{line_id}/calls/{call_id}
One call with its transcript, digits and code, audit-logged like a text. Always 404 on the sandbox line.
- Credential
- API key, OAuth token or session
- Scopes
calls:readandcalls:read_content- Answers
200{call}- Refusals
not_found
GET /v1/labels
The names your organization gave the numbers that text and call its lines, such as Chase for +18885550101. Each text and call from a named number carries its name as sender_label. A key limited to some lines cannot list them.
- Credential
- API key, OAuth token or session
- Scopes
messages:read- Answers
200{labels: [{sender, label, set_at}]}- Refusals
forbidden
PUT /v1/labels/{number}
Name a number, or rename it. The number may be written any way (+1 888 555 0101, 8885550101): Ostra matches it in E.164, a short code by its digits, an iMessage address in lower case. Every text and call from it then carries the name. Written to the audit log.
- Credential
- API key, OAuth token or session
- Scopes
labels:write- Request
{label}: 1 to 60 characters, no line breaks or control characters.- Answers
200{label: {sender, label, set_at}}- Refusals
invalid_requestconflictforbidden
DELETE /v1/labels/{number}
Remove a number's name: its texts and calls show the number alone again. Written to the audit log.
- Credential
- API key, OAuth token or session
- Scopes
labels:write- Answers
204
Webhooks
Endpoints that receive signed events for every line your organization holds.
- GET
/v1/webhooks - POST
/v1/webhooks - GET
/v1/webhooks/{endpoint_id} - PATCH
/v1/webhooks/{endpoint_id} - DELETE
/v1/webhooks/{endpoint_id} - POST
/v1/webhooks/{endpoint_id}/secret - POST
/v1/webhooks/{endpoint_id}/test - GET
/v1/webhooks/{endpoint_id}/deliveries - POST
/v1/webhooks/{endpoint_id}/deliveries/{delivery_id}/retry
GET /v1/webhooks
Your webhook endpoints.
- Credential
- API key, OAuth token or session
- Scopes
webhooks:read- Answers
200{endpoints}
POST /v1/webhooks
Create an endpoint. The signing secret is in this answer only. include_code is on unless you send false, for a credential holding messages:read_content.
- Credential
- API key, OAuth token or session
- Scopes
webhooks:write, plus the scopes of the events it carries- Idempotency-Key
- Accepted
- Request
{url, events, description?, include_code?, include_body?}- Answers
201{endpoint, secret}
GET /v1/webhooks/{endpoint_id}
One endpoint.
- Credential
- API key, OAuth token or session
- Scopes
webhooks:read- Answers
200Endpoint
PATCH /v1/webhooks/{endpoint_id}
Change events, description, include_code, include_body or enabled. Disabling fails every delivery still pending.
- Credential
- API key, OAuth token or session
- Scopes
webhooks:write, plus the scopes of the result- Answers
200Endpoint
DELETE /v1/webhooks/{endpoint_id}
Delete an endpoint. Nothing more is sent to it; its log stays readable for 30 days.
- Credential
- API key, OAuth token or session
- Scopes
webhooks:write- Answers
204
POST /v1/webhooks/{endpoint_id}/secret
Replace the signing secret at once. The new one is in this answer only.
- Credential
- API key, OAuth token or session
- Scopes
webhooks:write- Answers
200{secret}
POST /v1/webhooks/{endpoint_id}/test
Send a webhook.test event now.
- Credential
- API key, OAuth token or session
- Scopes
webhooks:write- Answers
202{delivery_id}- Refusals
forbiddenrate_limitedconflict
GET /v1/webhooks/{endpoint_id}/deliveries
The delivery log, newest first.
- Credential
- API key, OAuth token or session
- Scopes
webhooks:read- Request
?status=(pending, succeeded or failed) &limit= (1 to200).- Answers
200{deliveries}
POST /v1/webhooks/{endpoint_id}/deliveries/{delivery_id}/retry
Send a failed delivery again, once, now.
- Credential
- API key, OAuth token or session
- Scopes
webhooks:write- Answers
202{delivery_id}- Refusals
conflict
App devices
The Ostra apps register each device here and read its tunnel settings. Your own code rarely needs these.
- POST
/v1/devices - GET
/v1/devices - GET
/v1/devices/{device_id} - DELETE
/v1/devices/{device_id} - GET
/v1/devices/{device_id}/tunnel-config - PUT
/v1/devices/{device_id}/push - GET
/v1/devices/{device_id}/push - DELETE
/v1/devices/{device_id}/push - POST
/v1/voice/token - DELETE
/v1/voice/registration
POST /v1/devices
Register an Ostra app device for one line your organization holds: platform ios for the iPhone app, macos for the Mac app. The device makes its own key pair; only the public key is sent.
- Credential
- API key, OAuth token or session
- Scopes
devices:write- Idempotency-Key
- Accepted
- Request
{line_id, platform, public_key, name?}; any other field refuses the request.- Answers
201Device
GET /v1/devices
Your organization's devices, newest first, ended ones included.
- Credential
- API key, OAuth token or session
- Scopes
devices:read- Request
?line_id=&limit= (1 to 1000).- Answers
200{devices}
GET /v1/devices/{device_id}
One device.
- Credential
- API key, OAuth token or session
- Scopes
devices:read- Answers
200Device- Refusals
not_found
DELETE /v1/devices/{device_id}
Revoke a device. A revoked device is never admitted again.
- Credential
- API key, OAuth token or session
- Scopes
devices:write- Answers
204
GET /v1/devices/{device_id}/tunnel-config
Everything the app needs for its tunnel except its own private key, which never leaves the device.
- Credential
- API key, OAuth token or session
- Scopes
devices:read- Answers
200{interface, peer}- Refusals
device_revokedconflictgateway_pending
PUT /v1/devices/{device_id}/push
Store the device's push token, so the Ostra iPhone or Mac app is told of a text on its line.
- Credential
- API key, OAuth token or session
- Scopes
devices:write, plusmessages:read_contentwith show_code: true- Request
{apns_token, environment, show_code}- Answers
200{push, delivery}- Refusals
invalid_requestdevice_revoked
GET /v1/devices/{device_id}/push
The device's push registration, and whether Ostra sends pushes now.
- Credential
- API key, OAuth token or session
- Scopes
devices:read- Answers
200{push, delivery}
DELETE /v1/devices/{device_id}/push
Remove the device's push token.
- Credential
- API key, OAuth token or session
- Scopes
devices:write- Answers
204
POST /v1/voice/token
A Twilio Voice access token, incoming only: for one iPhone app install, so calls to the lines of the devices it registered ring it (only the credential that registered the devices may ask), or with kind browser for a signed-in dashboard tab, or kind mac for the Ostra Mac app's own sign-in, each ringing for the organization's lines while its five-minute lease runs.
- Credential
- API key, OAuth token or session
- Scopes
calls:readandcalls:read_contentanddevices:write- Request
{device_ids, environment}, or{kind: "browser", browser_id?}from a session, or{kind: "mac", browser_id?}from the Mac app- Answers
200{token, identity, expires_at, environment, ring_seconds, lines: [{device_id, line_id, rings}]}; for a browser200{token, identity, expires_at, browser_id, ring_seconds, lines: [{line_id, rings}]}
DELETE /v1/voice/registration
Stop calls ringing the app install that registered these devices (ringing turned off in the app), or a dashboard browser or a Mac by its browser_id. Only that install's own credential, or the browser's or Mac's own person, may ask.
- Credential
- API key, OAuth token or session
- Scopes
calls:read- Request
{device_ids}or{browser_id}- Answers
204- Refusals
invalid_requestforbiddennot_found
Organization and members
Managed from the dashboard with a signed-in session. API keys and OAuth tokens cannot call these, except GET /v1/org.
- GET
/v1/orgs - POST
/v1/orgs - GET
/v1/org - PATCH
/v1/org - GET
/v1/org/members - POST
/v1/org/members - PATCH
/v1/org/members/{user_id} - DELETE
/v1/org/members/{user_id} - GET
/v1/invitations - POST
/v1/invitations/{org_id}/accept - POST
/v1/invitations/{org_id}/decline
GET /v1/orgs
The organizations you belong to, with your role in each.
- Credential
- Signed-in session only
- Role
- Any signed-in person; no organization needed
- Answers
200{organizations}
POST /v1/orgs
Create an individual's account or a business organization with you as its owner. At most 5 per person, one of them an individual's.
- Credential
- Signed-in session only
- Role
- Any signed-in person; no organization needed
- Request
{name, kind?, turnstile_token?}- Answers
201Organization
GET /v1/org
Your organization: status, verification, the policy accepted, and whether Ostra may assign it a line. Any key, token or session of the organization may read it.
- Credential
- Any key, OAuth token or session of the organization; no scope
- Answers
200Organization
PATCH /v1/org
Rename the organization, or require two-step sign-in of every member (only an admin who has two-step on can turn that on).
- Credential
- Signed-in session only
- Role
- The admin role
- Request
{name?, require_two_step?}- Answers
200Organization- Refusals
conflict
GET /v1/org/members
The people in the organization, their roles, and whether each has two-step sign-in on; and, apart, the people invited who have not joined, with no two-step state of theirs.
- Credential
- Signed-in session only
- Role
- Any member
- Answers
200{members, invitations}
POST /v1/org/members
Invite a person by email with a role (an owner to invite an owner). They get an email, and become a member only by accepting from their own session; until then nothing of the organization applies to them. 20 a day, cancelled ones included.
- Credential
- Signed-in session only
- Role
- The admin role
- Request
{email, role}- Answers
201{user_id, email, role, invited_at, status, notice_sent}- Refusals
conflictrate_limited
PATCH /v1/org/members/{user_id}
Change a person's role (an owner when the member is or becomes an owner). The last owner cannot be changed.
- Credential
- Signed-in session only
- Role
- The admin role
- Request
{role}- Answers
200{user_id, role}
DELETE /v1/org/members/{user_id}
Remove yourself, or, as an admin, another person (an owner to remove an owner). The last owner cannot be removed. Revokes the OAuth access they granted for the organization. For a person invited and not yet joined, an admin cancels the invitation (an owner for an owner's).
- Credential
- Signed-in session only
- Role
- Any member
- Answers
204
GET /v1/invitations
Your own open invitations: each organization, the role, when, and the address of the admin who sent it. A closed organization's are left out.
- Credential
- Signed-in session only
- Role
- Any signed-in person; no organization needed
- Answers
200{invitations}
POST /v1/invitations/{org_id}/accept
Accept an invitation sent to you: you become a member with the role it names, and from then on the organization counts at your sign-in, its two-step requirement included. Only the invited person's own session can.
- Credential
- Signed-in session only
- Role
- Any signed-in person; no organization needed
- Answers
200{id, name, role, status, kind, internal}
POST /v1/invitations/{org_id}/decline
Decline an invitation sent to you. It ends; the organization can send another.
- Credential
- Signed-in session only
- Role
- Any signed-in person; no organization needed
- Answers
204- Refusals
not_found
Verification and policy
Signing up: the details, the mobile number proved by a texted code, and the automatic check (the sanctions screen, and where the request comes from); acceptance of the acceptable use policy.
- GET
/v1/signup/turnstile - GET
/v1/aup - GET
/v1/org/verification - PUT
/v1/org/verification - POST
/v1/org/verification/submit - GET
/v1/org/verification/phone - POST
/v1/org/verification/phone - POST
/v1/org/verification/phone/check - POST
/v1/org/aup
GET /v1/signup/turnstile
Whether the sign-up steps (creating an account, texting the mobile code, submitting) take a Cloudflare Turnstile token on this server, and the widget's public site key.
- Credential
- None
- Answers
200{required, site_key}
GET /v1/aup
The acceptable use policy in force: its version and where to read it.
- Credential
- None
- Answers
200{version, url}
GET /v1/org/verification
The sign-up record as saved: the details, the verified mobile number (its last four only to a member who is not an admin), and its status.
- Credential
- Signed-in session only
- Role
- Any member
- Answers
200Verification
PUT /v1/org/verification
Save the draft: account type, your legal name, a business's legal name and a US address. Fields sent replace what is saved, fields left out keep their value.
- Credential
- Signed-in session only
- Role
- The admin role
- Answers
200Verification
POST /v1/org/verification/submit
Submit with the 18-or-older statement and the policy in force. The automatic check runs at once: approved, or held for a person at Ostra.
- Credential
- Signed-in session only
- Role
- The admin role
- Request
{attestations, turnstile_token?}- Answers
200Verification
GET /v1/org/verification/phone
How the mobile number step works here: whether codes are really texted (twilio) or held for Ostra's staff (outbox), how long a code lasts, how many tries it takes.
- Credential
- Signed-in session only
- Role
- Any member
- Answers
200{mode, code_ttl_seconds, max_attempts}
POST /v1/org/verification/phone
Text a 6-digit code to a US mobile number. Internet-phone (VoIP) numbers, landlines and numbers Ostra uses are refused before anything is sent.
- Credential
- Signed-in session only
- Role
- The admin role
- Request
{phone, turnstile_token?}- Answers
202{sent, number_last4, expires_at, mode}
POST /v1/org/verification/phone/check
Prove the number with the code from the text: the record then holds it as verified.
- Credential
- Signed-in session only
- Role
- The admin role
- Request
{code}- Answers
200Verification
POST /v1/org/aup
Record your acceptance of the policy in force, with the time and your address.
- Credential
- Signed-in session only
- Role
- The admin role
- Request
{version}- Answers
201{version, accepted_at, accepted_by}
API keys
Made and revoked by an admin from a signed-in session. A key can never create or see another key.
GET /v1/keys
The organization's API keys, each with its prefix only.
- Credential
- Signed-in session only
- Role
- The admin role
- Answers
200{keys}
POST /v1/keys
Make a key with a name, its scopes, and optionally some lines and an expiry. The secret is in this answer only.
- Credential
- Signed-in session only
- Role
- The admin role
- Request
{name, scopes, line_ids?, expires_at?}- Answers
201{key, secret}- Refusals
conflict
DELETE /v1/keys/{key_id}
Revoke a key. It stops working at once.
- Credential
- Signed-in session only
- Role
- The admin role
- Answers
204
Billing
A signed-in session only. Checkout and the card portal are Stripe's own pages.
GET /v1/billing
Where billing stands: the price sheet in force, the card on file, each line with its own subscription and renewal date (soonest first), any grace period or suspension, any invoice a canceled line left unpaid, the usage subscription, and the calls billed so far this period.
- Credential
- Signed-in session only
- Role
- Any member
- Answers
200Billing
POST /v1/lines/{line_id}/cancel
Cancel a line: it ends now, its subscription ends with no refund of the rest of the month, and its number is deleted, never given to another Ostra customer. Send it only after showing that the number is deleted. It needs a sign-in, or a second step, from the last 10 minutes, so an old session cannot delete a number.
- Credential
- Signed-in session only
- Role
- The admin role
- Request
{confirm: "delete_number"}- Answers
200{line_id, ended_at, number, billing}
POST /v1/billing/checkout
Open Stripe Checkout in setup mode to save a card; nothing is charged until Ostra assigns a line. While one is still open, the same one comes back.
- Credential
- Signed-in session only
- Role
- The admin role
- Request
{}- Answers
201{url, expires_at}
POST /v1/billing/portal
Open Stripe's Customer Portal for the card and the invoices, where an unpaid invoice is paid. Refused while the portal would let a customer end or change a line's subscription there.
- Credential
- Signed-in session only
- Role
- The admin role
- Request
{}- Answers
200{url}
Sign-in
Passwordless sign-in for the dashboard: a 6-digit code by email (with a link that asks before it signs in), then a session cookie.
- GET
/v1/auth/access - POST
/v1/auth/access - POST
/v1/auth/sign-in - POST
/v1/auth/sign-in/code - POST
/v1/auth/sign-in/check - POST
/v1/auth/sign-in/verify - GET
/v1/auth/session - POST
/v1/auth/sign-out - POST
/v1/auth/two-step - POST
/v1/auth/two-step/enroll - POST
/v1/auth/two-step/enroll/confirm - GET
/v1/auth/two-step - POST
/v1/auth/two-step/setup - POST
/v1/auth/two-step/setup/confirm - POST
/v1/auth/two-step/backup-codes - POST
/v1/auth/two-step/disable
GET /v1/auth/access
Whether Ostra is open by invitation on this server (the access gate), and whether this browser holds a pass from an access code, until when. A pass whose code was revoked or ended is cleared.
- Credential
- None
- Answers
200{required, unlocked, expires_at}
POST /v1/auth/access
An access code for this browser's pass, an HttpOnly cookie for 30 days or until the code ends. While the access gate is on, every route that makes an account or a session takes the pass. 10 wrong codes from a client network in an hour stop its code checks for the hour; across Ostra, 300 wrong in an hour stop them while Turnstile is off, and 3,000 whatever a code carries.
- Credential
- None
- Request
{code, turnstile_token?}as application/json. Case, spaces and dashes in the code are ignored. While Cloudflare Turnstile is on, turnstile_token is a token from the widget drawn with the action access_code.- Answers
200{required, unlocked: true, expires_at, room}, and sets the pass cookie. room is false for a code that has let in as many people as it allows: its pass opens sign-in for those people only.
POST /v1/auth/sign-in
Email a 6-digit sign-in code and a link, for 15 minutes; older ones for the address stop working. The answer is the same whether or not the address has an account.
- Credential
- None
- Request
{email, turnstile_token?}as application/json. While Cloudflare Turnstile is on, a network's fourth sign-in email in an hour needs turnstile_token from the widget drawn with the action sign_in.- Answers
202{sent: true}
POST /v1/auth/sign-in/code
Trade the address and its emailed code for a session cookie. 5 wrong codes end a code; 10 per address an hour, 30 per address a day, or 30 per client network an hour stop code checks there until the oldest leaves its window.
- Credential
- None
- Request
{email, code}as application/json; spaces and dashes in the code are ignored.- Answers
200Session, and sets the session cookie.
POST /v1/auth/sign-in/check
Whose sign-in link it is, without using it: the page the link opens asks before it signs in.
- Credential
- None
- Request
{token}(the osl_ token from the link's fragment).- Answers
200{email, expires_at}- Refusals
invalid_token
POST /v1/auth/sign-in/verify
Trade the link's token for a session cookie, when the person taps Sign in.
- Credential
- None
- Request
{token}(the osl_ token from the link's fragment).- Answers
200Session, and sets the session cookie.
GET /v1/auth/session
Who is signed in, their organizations, and the CSRF token to send on every change.
- Credential
- Signed-in session only
- Role
- Any signed-in person; no organization needed
- Answers
200Session- Refusals
unauthenticated
POST /v1/auth/sign-out
End the session and clear the cookie.
- Credential
- Signed-in session only
- Role
- Any signed-in person; no organization needed
- Answers
204
POST /v1/auth/two-step
The second step: when the email code answers a two-step challenge, trade its token and a code from the authenticator app (or a backup code) for the session cookie. Each code works once; 5 wrong end the challenge, and the person's and the network's limits are those of sign-in codes.
- Credential
- None
- Request
{token, code}or{token, backup_code}as application/json.- Answers
200Session, and sets the session cookie; with a backup code also backup_codes_left.
POST /v1/auth/two-step/enroll
When an organization requires two-step sign-in and the person has not set it up: a new authenticator secret and its otpauth URI, for a QR code.
- Credential
- None
- Request
{token}(the enrollment challenge) as application/json.- Answers
200{secret, otpauth_uri}- Refusals
invalid_codeconflictaccess_code_required
POST /v1/auth/two-step/enroll/confirm
Confirm the new secret with one code: two-step is on, the session cookie is set, and ten backup codes are shown this once.
- Credential
- None
- Request
{token, code}as application/json.- Answers
200Session, sets the session cookie, and backup_codes.
GET /v1/auth/two-step
Whether two-step sign-in is on for you, how many backup codes are left, and which of your organizations require it.
- Credential
- Signed-in session only
- Role
- Any signed-in person; no organization needed
- Answers
200{enabled, enabled_at, backup_codes_left, required_by}
POST /v1/auth/two-step/setup
Start turning two-step sign-in on: a new authenticator secret and its otpauth URI, pending until a code confirms it.
- Credential
- Signed-in session only
- Role
- Any signed-in person; no organization needed
- Answers
200{secret, otpauth_uri}- Refusals
conflict
POST /v1/auth/two-step/setup/confirm
Confirm with one code: two-step is on, ten backup codes are shown this once, and your other sessions end.
- Credential
- Signed-in session only
- Role
- Any signed-in person; no organization needed
- Request
{code}- Answers
200{enabled, backup_codes}- Refusals
invalid_coderate_limited
POST /v1/auth/two-step/backup-codes
Ten new backup codes, for a current code from the app; every old one stops working.
- Credential
- Signed-in session only
- Role
- Any signed-in person; no organization needed
- Request
{code}- Answers
200{backup_codes}- Refusals
invalid_codeconflictrate_limited
POST /v1/auth/two-step/disable
Turn two-step sign-in off with a code or a backup code: the secret and the backup codes are deleted and your other sessions end. Refused while an organization of yours requires it.
- Credential
- Signed-in session only
- Role
- Any signed-in person; no organization needed
- Request
{code}or{backup_code}- Answers
200{enabled}- Refusals
invalid_codeconflictrate_limited
OAuth
Authorization code with PKCE for the apps, the extension and MCP clients. Clients are registered by Ostra.
- GET
/.well-known/oauth-authorization-server - GET
/.well-known/oauth-protected-resource - GET
/oauth/authorize - GET
/v1/oauth/authorize - POST
/v1/oauth/authorize - POST
/oauth/token - POST
/oauth/revoke - POST
/oauth/introspect - GET
/v1/oauth/grants - DELETE
/v1/oauth/grants/{grant_id}
GET /.well-known/oauth-authorization-server
OAuth authorization server metadata (RFC 8414).
- Credential
- None
- Answers
200{issuer, authorization_endpoint, token_endpoint, ...}
GET /.well-known/oauth-protected-resource
Protected resource metadata for the REST API (RFC 9728).
- Credential
- None
- Answers
200{resource, authorization_servers, scopes_supported, bearer_methods_supported}
GET /oauth/authorize
Start of the authorization code flow. Checks client_id and redirect_uri, then sends the browser to the dashboard's consent screen. A wrong client or redirect is 400 and never redirects.
- Credential
- None
- Request
?response_type=code&client_id=&redirect_uri=&scope=&state=&code_challenge=&code_challenge_method=S256&resource=- Answers
302
GET /v1/oauth/authorize
For the consent screen: the client, the scopes asked for, and the organizations you may grant them for.
- Credential
- Signed-in session only
- Role
- Any signed-in person; no organization needed
- Answers
200{client, redirect_uri, state, scopes, resource, organizations}
POST /v1/oauth/authorize
Approve the request for one organization you are a member of (any other is 404), or deny it.
- Credential
- Signed-in session only
- Role
- Any signed-in person; no organization needed
- Request
{client_id, redirect_uri, state?, scope, code_challenge, code_challenge_method, resource?, organization_id, decision}- Answers
200{redirect_to}
POST /oauth/token
Trade a code (with its code_verifier) or a refresh token for tokens. Form-encoded.
- Credential
- OAuth client (form-encoded)
- Request
- grant_type=authorization_code with code, redirect_uri, client_id, code_verifier; or grant_type=refresh_token with refresh_token, client_id.
- Answers
200{access_token, token_type, expires_in, refresh_token, scope}; errors per RFC 6749 and RFC 8707.
POST /oauth/revoke
Revoke a token (RFC 7009). Form-encoded.
- Credential
- OAuth client (form-encoded)
- Request
- token, token_type_hint?
- Answers
200
POST /oauth/introspect
Token introspection (RFC 7662), for a confidential client Ostra allowed to introspect.
- Credential
- OAuth client (form-encoded)
- Answers
200{active, ...}
GET /v1/oauth/grants
The apps you approved for the organization.
- Credential
- Signed-in session only
- Role
- Any member
- Answers
200{grants}
DELETE /v1/oauth/grants/{grant_id}
Revoke an app's access and every token issued from it.
- Credential
- Signed-in session only
- Role
- Any member
- Answers
204
MCP server
Its own host and listener. One JSON-RPC message per POST.
- POST
/mcp - GET
/mcp - DELETE
/mcp - GET
/.well-known/oauth-protected-resource - GET
/.well-known/oauth-protected-resource/mcp
POST mcp/mcp
The MCP endpoint: one JSON-RPC message per POST, answered with one JSON object or a short event stream. A batch is refused.
- Credential
- API key, or an OAuth token issued for the MCP server
- Scopes
- Each tool’s own (see the MCP page)
- Answers
200;401or403with WWW-Authenticate when the credential is missing, refused or lacks a scope.
GET mcp/mcp
No sessions and no standing stream.
- Credential
- API key, or an OAuth token issued for the MCP server
- Scopes
- Each tool’s own (see the MCP page)
- Answers
405
DELETE mcp/mcp
No sessions to end.
- Credential
- API key, or an OAuth token issued for the MCP server
- Scopes
- Each tool’s own (see the MCP page)
- Answers
405
GET mcp/.well-known/oauth-protected-resource
Protected resource metadata for the MCP server (RFC 9728).
- Credential
- None
- Answers
200{resource, authorization_servers, scopes_supported, bearer_methods_supported, resource_name}
GET mcp/.well-known/oauth-protected-resource/mcp
The same metadata at the path the MCP specification names for the /mcp resource.
- Credential
- None
- Answers
200, as above