API Reference
https://tinetech.org/v1
Overview
The TINE REST API lets you integrate phone numbers, messaging, calls, voicemails, and billing into your own systems. All requests target the base URL:
https://tinetech.org/v1
Every authenticated request deducts $0.001 from your credit balance, plus any action cost (sending an SMS, purchasing a number, etc.). See Pricing for exactly which requests are charged.
A machine-readable OpenAPI 3.1 spec is at https://tinetech.org/v1/openapi.json. Import it into Postman, Insomnia or a code generator, or give it to an AI coding assistant.
Authentication
Pass your API key in the Authorization header as a Bearer token. Keys are created on the Developers page and start with tine_live_.
curl https://tinetech.org/v1/balance \ -H "Authorization: Bearer tine_live_xxxxxxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Requests without a valid key return 401 authentication_failed. Revoked or expired keys return the same error.
Scopes
Each key is issued with one or more scopes. Calling an endpoint without the required scope returns 403 permission_denied.
numbers:read
List owned phone numbers and search available numbers to purchase.messages:read
List messages and conversation history.messages:send
Send SMS and MMS from numbers you own.calls:read
List the call log and fetch call recordings.billing:read
Read current credit balance and transaction history.voice:call
Generate browser voice tokens and purchase phone numbers via API.Response envelope
Every successful response is HTTP 200. Single-resource endpoints wrap the result in data: {}. List endpoints wrap results in data: [] and include a next_cursor field.
// Single-resource
{
"data": { ... }
}
// List
{
"data": [ ... ],
"next_cursor": "2026-09-18T12:00:00.000Z" // null when no further pages exist
}Every response — success or error — includes an X-Request-Id response header. Include this value in any support request so we can trace the exact call.
Errors
Non-2xx responses include a JSON body with a machine-readable code field. Always branch on code, not on the English message string, which may change.
{
"error": {
"code": "insufficient_funds",
"message": "Insufficient credit balance to cover the per-request API fee ...",
"request_id": "req_a1b2c3d4e5f6g7h8i9j0"
}
}400
invalid_request
A required parameter is missing or malformed.401
authentication_failed
The API key is missing, invalid, or revoked.402
insufficient_funds
Balance is too low to cover the per-request fee or action cost (SMS, number purchase, etc.).402
subscription_required
An active Developer subscription is required to use the API.403
permission_denied
The key does not have the required scope for this endpoint.404
not_found
The requested resource does not exist or does not belong to your account.409
idempotency_in_progress
A request with the same Idempotency-Key is still running. Retry shortly.422
unprocessable
The request is structurally valid but the action could not be completed — for example, Twilio rejected the message or the destination number is blocked.422
idempotency_key_reused
The Idempotency-Key was already used with a different request body.429
rate_limited
Too many requests for this key. Wait for the number of seconds in Retry-After.500
internal_error
An unexpected server error occurred.500
provider_error
An upstream provider error occurred (e.g. Twilio auth failure).Pagination
List endpoints use keyset (cursor) pagination. Results are always returned in reverse-chronological order. When there are more results, the response includes a non-null next_cursor. Pass it as the before query parameter on the next request to fetch the following page.
# Page 1 curl "https://tinetech.org/v1/calls?limit=25" \ -H "Authorization: Bearer tine_live_..." # Page 2 — pass next_cursor from the page 1 response as before= curl "https://tinetech.org/v1/calls?limit=25&before=2026-09-15T08:30:00.000Z" \ -H "Authorization: Bearer tine_live_..."
When next_cursor is null, you have reached the end of the list. The cursor type varies per endpoint: ISO 8601 timestamp for Calls, Voicemails, and Billing; Unix milliseconds for Messages.
Pricing
All costs are deducted from your prepaid credit balance. Top up at any time from the Dashboard.
Each monthly payment adds $40 of plan credit. Usage spends plan credit first. Plan credit doesn't carry over: whatever is left when your plan renews expires and is replaced by the new month's $40. Auto-recharge credit is yours to keep and rolls over from month to month.
US and Canadian numbers
Per API request (any endpoint)
$0.001Outbound SMS
$0.05 / segmentInbound SMS
$0.025 / segmentOutbound MMS
$0.07 / messageInbound MMS
$0.04 / messageOutbound call
$0.05 / minInbound call
$0.03 / minCall recording
$0.02 / minPhone number purchase
$5.00 one-timePhone number monthly fee
$5.00 / monthUK (+44) numbers
Outbound SMS
$0.09 / segmentInbound SMS
$0.025 / segmentOutbound call
$0.035 / minInbound call
$0.025 / minPhone number purchase
$8.00 one-timePhone number monthly fee
$8.00 / monthUK numbers can't send or receive MMS. The per-request fee and call recording cost the same as for US and Canadian numbers.
The per-request fee is charged once the key, scope and rate limit checks pass and before the handler runs, so a request the handler rejects still costs $0.001. Requests that fail authentication, lack the scope, hit a rate limit or are replayed from an Idempotency-Key are not charged. Action costs (SMS, calls) are charged separately on top of that. Call recording is automatic on all connected calls and charged per minute of recorded audio.
Rate limits
Limits are counted per API key in fixed windows. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds) for the tightest limit that applies. Going over returns 429 rate_limited with a Retry-After header in seconds. Rate-limited requests are not charged.
Every endpoint
600 requests / minutePOST /v1/messages
60 messages / minutePOST /v1/numbers/purchase
20 purchases / hourNeed more? Email pluto@tinesim.com with your use case and expected volume.
Idempotency
POST /v1/messages and POST /v1/numbers/purchase accept an Idempotency-Key header (any string up to 255 characters; a UUID works well). Send one on every request you might retry, such as after a timeout. If a request with the same key and the same body already finished in the last 24 hours, you get its original response back with an Idempotent-Replayed: true header: no second message is sent, no second number is bought and nothing is charged.
curl https://tinetech.org/v1/messages \
-H "Authorization: Bearer tine_live_..." \
-H "Idempotency-Key: 6f1c2d0e-7b9a-4c55-9a3e-2f0d8c1b7a41" \
-H "Content-Type: application/json" \
-d '{"from": "+12125550100", "to": "+12125550199", "body": "Your code is 4821"}'Same key, same body, first request finished
The stored response, with Idempotent-Replayed: trueSame key, same body, first request still running
409 idempotency_in_progress. Retry after a couple of seconds.Same key, different body
422 idempotency_key_reusedFirst request got 402, 429 or 503
Nothing is stored, so the same key can be retried once you have topped up or waited.Keys are scoped to your account and endpoint. Requests without the header behave as before.
Endpoints
Balance
/v1/balance
Returns your current credit balance in USD. plan_credit is the part of the balance that is this month's plan credit, which expires at plan_credit_expires_at; the rest is auto-recharge credit and rolls over. Required scope: billing:read.
Response
{
"data": {
"balance": 52.07,
"currency": "USD",
"plan_credit": 12.07,
"plan_credit_expires_at": "2026-11-18T00:00:00.000Z"
}
}Example
curl https://tinetech.org/v1/balance \ -H "Authorization: Bearer tine_live_..."
Numbers
List your numbers
/v1/numbers
Returns all phone numbers owned by your account, whether purchased through the portal or the API. Required scope: numbers:read.
Example
curl https://tinetech.org/v1/numbers \ -H "Authorization: Bearer tine_live_..."
{
"data": [
{
"phone_number": "+12125551234",
"country_code": "US",
"is_primary": true,
"subscription_status": "active",
"twilio_sid": "PNxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"purchased_at": "2026-08-01T09:30:00.000Z"
},
{
"phone_number": "+12125559876",
"country_code": "US",
"is_primary": false,
"subscription_status": "active",
"twilio_sid": "PNyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
"purchased_at": "2026-09-12T10:00:00.000Z"
}
]
}Search available numbers
/v1/numbers/available
Searches Twilio's inventory for purchasable phone numbers. All returned numbers are SMS- and voice-enabled. US numbers are also MMS-enabled. Required scope: numbers:read.
country
area_code
contains
limit
Example
curl "https://tinetech.org/v1/numbers/available?country=US&area_code=212&limit=5" \ -H "Authorization: Bearer tine_live_..."
{
"data": [
{
"phone_number": "+12125550001",
"friendly_name": "(212) 555-0001",
"country_code": "US",
"region": "NY",
"capabilities": {
"sms": true,
"voice": true,
"mms": true
}
}
]
}Purchase a number
/v1/numbers/purchase
Purchases a phone number and registers it on your account. Webhooks for inbound SMS and calls are configured automatically. Costs $5.00 from your balance immediately, plus a recurring $5.00/month fee. Required scope: voice:call.
phone_number
Example
curl -X POST https://tinetech.org/v1/numbers/purchase \
-H "Authorization: Bearer tine_live_..." \
-H "Content-Type: application/json" \
-d '{"phone_number": "+12125550001"}'{
"data": {
"phone_number": "+12125550001",
"country_code": "US",
"twilio_sid": "PNxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"purchased_at": "2026-09-19T04:50:00.000Z"
}
}Messages
List messages
/v1/messages
Returns messages across all of your numbers in reverse-chronological order, including both messages you received and messages you sent (from the portal, the mobile app, or POST /v1/messages). Each message has a direction of inbound or outbound; it is null only for legacy records whose direction can no longer be determined. Required scope: messages:read.
number
limit
before
Example
curl "https://tinetech.org/v1/messages?number=%2B12125551234&limit=20" \ -H "Authorization: Bearer tine_live_..."
{
"data": [
{
"id": "msg_abc123",
"direction": "outbound",
"from": "+12125551234",
"to": "+19175550000",
"body": "Hello from TINE!",
"media_url": [],
"status": "delivered",
"message_sid": "SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"is_read": true,
"created_at": "2026-09-18T10:00:00.000Z"
}
],
"next_cursor": null
}Send a message
/v1/messages
Sends an SMS or MMS. The from number must be owned by your account. If both body and media_url are supplied, the message is sent as MMS. Required scope: messages:send.
Long SMS are split into segments and billed per segment. One segment holds 160 characters, or 70 if the text contains characters outside the standard SMS alphabet (for example Cyrillic or emoji). Longer messages use 153 or 67 characters per segment. Messages over 10 segments are rejected with 400. The response includes segments and the price charged. MMS is billed per message.
from
to
body
media_url
Example — SMS
curl -X POST https://tinetech.org/v1/messages \
-H "Authorization: Bearer tine_live_..." \
-H "Content-Type: application/json" \
-d '{
"from": "+12125551234",
"to": "+19175550000",
"body": "Hello from TINE!"
}'{
"data": {
"id": "SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"direction": "outbound",
"from": "+12125551234",
"to": "+19175550000",
"body": "Hello from TINE!",
"media_url": [],
"status": "queued",
"message_sid": "SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"segments": 1,
"price": 0.05,
"created_at": "2026-09-19T04:50:00.000Z"
}
}Example — MMS
curl -X POST https://tinetech.org/v1/messages \
-H "Authorization: Bearer tine_live_..." \
-H "Content-Type: application/json" \
-d '{
"from": "+12125551234",
"to": "+19175550000",
"body": "Check this out",
"media_url": ["https://example.com/photo.jpg"]
}'Calls
List calls
/v1/calls
Returns your call log in reverse-chronological order. Required scope: calls:read.
number
direction
limit
before
Example
curl "https://tinetech.org/v1/calls?direction=outbound&limit=10" \ -H "Authorization: Bearer tine_live_..."
{
"data": [
{
"id": "CAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"from": "+12125551234",
"to": "+19175550000",
"direction": "outbound",
"status": "completed",
"duration_seconds": 93,
"created_at": "2026-09-18T09:00:00.000Z"
}
],
"next_cursor": null
}Get call recording
/v1/calls/{callSid}/recording
Returns the recording URL and metadata for a specific call. Returns 404 not_found if the call has no recording or does not belong to your account. Required scope: calls:read.
callSid
Example
curl https://tinetech.org/v1/calls/CAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/recording \ -H "Authorization: Bearer tine_live_..."
{
"data": {
"call_sid": "CAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"recording_url": "https://api.twilio.com/2010-04-01/Accounts/.../Recordings/RE....mp3",
"recording_duration": 93,
"has_voicemail": false
}
}Voicemails
/v1/voicemails
Returns voicemails in reverse-chronological order. Required scope: calls:read.
limit
before
Example
curl https://tinetech.org/v1/voicemails \ -H "Authorization: Bearer tine_live_..."
{
"data": [
{
"id": "vm_abc123",
"call_sid": "CAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"from": "+19175550000",
"to": "+12125551234",
"recording_url": "https://api.twilio.com/2010-04-01/Accounts/.../Recordings/RE....mp3",
"recording_duration": 24,
"listened": false,
"created_at": "2026-09-17T14:30:00.000Z"
}
],
"next_cursor": null
}Billing history
/v1/billing/history
Returns your credit transaction ledger — top-ups, outbound/inbound SMS and MMS deductions, and voice deductions — in reverse-chronological order. The flat $0.001 per-request fee is not itemized here; it is aggregated daily and visible in the portal usage graph. Required scope: billing:read.
limit
before
Transaction types
topup
Credit Top-Up
Balance was topped up (manual or auto-recharge).sms_outbound
Outbound SMS
An outbound SMS was sent ($0.05 per segment).sms_inbound
Inbound SMS
An inbound SMS was received ($0.025 per segment).mms_outbound
Outbound MMS
An outbound MMS was sent ($0.07).adjustment
Balance Adjustment
TINE support removed credit from your balance.Example
curl https://tinetech.org/v1/billing/history \ -H "Authorization: Bearer tine_live_..."
{
"data": [
{
"id": "txn_abc123",
"type": "sms_outbound",
"type_label": "Outbound SMS",
"amount": -0.05,
"balance_after": 20.02,
"description": "SMS to +19175550000",
"created_at": "2026-09-18T10:00:00.000Z"
},
{
"id": "txn_abc124",
"type": "topup",
"type_label": "Credit Top-Up",
"amount": 50.00,
"balance_after": 70.07,
"description": "Auto-recharge",
"created_at": "2026-09-17T08:00:00.000Z"
}
],
"next_cursor": null
}Voice token
/v1/voice/token
Issues a short-lived Twilio Access Token (TTL: 3 600 seconds) that you pass to the @twilio/voice-sdk Device constructor to make and receive browser-based calls. The identity is prefixed dev:{userId} so inbound call routing works alongside portal sessions without collision. Required scope: voice:call.
This endpoint takes no request body. You must refresh the token before it expires — call this endpoint again to get a new one.
Example
curl -X POST https://tinetech.org/v1/voice/token \ -H "Authorization: Bearer tine_live_..."
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"identity": "dev:uid_abc123",
"ttl": 3600
}Using the token (JavaScript)
import { Device } from '@twilio/voice-sdk';
// 1. Get a token from your backend (don't expose your API key client-side)
const res = await fetch('/your-backend/voice-token', { method: 'POST' });
const { token } = await res.json();
// 2. Initialise the Device
const device = new Device(token, { logLevel: 1 });
await device.register();
// 3. Make an outbound call
const call = await device.connect({
params: { To: '+19175550000' },
});
// 4. Refresh before expiry (3600 s)
device.updateToken(await fetchNewToken());Webhooks
Register one or more HTTPS endpoints on the Developers page. TINE will POST a signed JSON payload to each registered URL when an event occurs on your account. Each endpoint has its own signing secret shown once at creation time.
Events
message.received
An inbound SMS or MMS arrived on one of your numbers.message.delivered
The carrier confirmed an outbound message was delivered.message.failed
An outbound message could not be delivered (status failed or undelivered).call.completed
A call ended — any direction, any outcome (completed, no-answer, busy, failed, canceled).webhook.test
Sent only when you press "Send test event" on the Developers page.Each occurrence is delivered once per endpoint. Delivery is at-least-once, so the same event id can occasionally arrive twice.
Payload shape
Every delivery is a JSON POST with the same envelope. data mirrors the matching API resource.
{
"id": "evt_6f1c0a9e2b7d4c3a1e5f0b8d",
"event": "message.received",
"created_at": "2026-10-03T10:00:00.000Z",
"data": {
"id": "-OaB3xYz12AbCdEfGhIj",
"message_sid": "SM0123456789abcdef0123456789abcdef",
"direction": "inbound",
"from": "+14155550123",
"to": "+12025550188",
"body": "Hi, is this still available?",
"media_url": [],
"segments": 1,
"status": "received",
"created_at": "2026-10-03T10:00:00.000Z"
}
}message.delivered and message.failed: data.id is the same id returned by POST /v1/messages.
"data": {
"id": "SM0123456789abcdef0123456789abcdef",
"message_sid": "SM0123456789abcdef0123456789abcdef",
"direction": "outbound",
"from": "+12025550188",
"to": "+14155550123",
"status": "undelivered", // "delivered" for message.delivered
"error_code": 30003 // carrier error code, null when delivered
}call.completed: data.id matches the call in GET /v1/calls. duration_seconds is talk time (0 if unanswered).
"data": {
"id": "CA0123456789abcdef0123456789abcdef",
"direction": "inbound",
"from": "+14155550123",
"to": "+12025550188",
"status": "completed", // or no-answer, busy, failed, canceled
"duration_seconds": 74,
"created_at": "2026-10-03T09:58:41.000Z",
"ended_at": "2026-10-03T10:00:02.000Z"
}Delivery headers
X-Tine-Timestamp
Unix seconds at the time of delivery.X-Tine-Signature
v1=<hex>. HMAC-SHA256 of "<timestamp>.<rawBody>" signed with your endpoint's signing secret.X-Tine-Event
The event type, e.g. message.received. Same as the body's event field.X-Tine-Delivery
Unique id for this delivery. Stays the same across retries.Verifying signatures
Always verify the signature before processing a webhook. Reject any delivery where the computed HMAC does not match, or where the timestamp is more than 5 minutes old (replay attack protection).
import crypto from 'crypto';
export function verifyWebhook(signingSecret, req) {
const timestamp = req.headers['x-tine-timestamp'];
const sigHeader = req.headers['x-tine-signature']; // "v1=<hex>"
const rawBody = req.rawBody; // unparsed request body string
// 1. Reject stale deliveries (> 5 min)
const age = Math.floor(Date.now() / 1000) - Number(timestamp);
if (age > 300) throw new Error('Webhook timestamp too old');
// 2. Compute expected signature
const payload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac('sha256', signingSecret)
.update(payload)
.digest('hex');
// 3. Constant-time comparison
const received = (sigHeader ?? '').replace('v1=', '');
const valid = crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(received.padEnd(expected.length, '0'), 'hex'),
);
if (!valid) throw new Error('Invalid webhook signature');
}import hmac, hashlib, time
def verify_webhook(signing_secret: str, timestamp: str, raw_body: str, sig_header: str) -> bool:
# Reject stale deliveries (> 5 min)
age = int(time.time()) - int(timestamp)
if age > 300:
raise ValueError("Webhook timestamp too old")
payload = f"{timestamp}.{raw_body}"
expected = hmac.new(signing_secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
received = sig_header.replace("v1=", "")
if not hmac.compare_digest(expected, received):
raise ValueError("Invalid webhook signature")
return TrueRetry policy
TINE expects your endpoint to return a 2xx response within 10 seconds. Redirects are not followed. Non-2xx responses, timeouts and connection errors are retried up to 5 times, after about 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. If all 6 attempts fail the delivery is abandoned. Every attempt, including failures, is listed under Recent deliveries on the Developers page. Make your handler idempotent on the id field to handle duplicate deliveries safely.