API Reference

https://tinetech.org/v1

Developer dashboard and API keys
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:

text
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_.

bash
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.

json
// 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.

json
{
  "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.

bash
# 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.001

Outbound SMS

$0.05 / segment

Inbound SMS

$0.025 / segment

Outbound MMS

$0.07 / message

Inbound MMS

$0.04 / message

Outbound call

$0.05 / min

Inbound call

$0.03 / min

Call recording

$0.02 / min

Phone number purchase

$5.00 one-time

Phone number monthly fee

$5.00 / month

UK (+44) numbers

Outbound SMS

$0.09 / segment

Inbound SMS

$0.025 / segment

Outbound call

$0.035 / min

Inbound call

$0.025 / min

Phone number purchase

$8.00 one-time

Phone number monthly fee

$8.00 / month

UK 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 / minute

POST /v1/messages

60 messages / minute

POST /v1/numbers/purchase

20 purchases / hour

Need 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.

bash
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: true

Same key, same body, first request still running

409 idempotency_in_progress. Retry after a couple of seconds.

Same key, different body

422 idempotency_key_reused

First 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
GET

/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

json
{
  "data": {
    "balance": 52.07,
    "currency": "USD",
    "plan_credit": 12.07,
    "plan_credit_expires_at": "2026-11-18T00:00:00.000Z"
  }
}

Example

bash
curl https://tinetech.org/v1/balance \
  -H "Authorization: Bearer tine_live_..."
Numbers

List your numbers

GET

/v1/numbers

Returns all phone numbers owned by your account, whether purchased through the portal or the API. Required scope: numbers:read.

Example

bash
curl https://tinetech.org/v1/numbers \
  -H "Authorization: Bearer tine_live_..."
json
{
  "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

GET

/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.

Query parameters

country

stringISO 3166-1 alpha-2 country code. Default: US. Alias UK is accepted (mapped to GB).

area_code

string3-digit area code to filter by. US numbers only.

contains

stringPattern the number must contain, e.g. 555.

limit

integerResults to return. Range: 1–30. Default: 10.

Example

bash
curl "https://tinetech.org/v1/numbers/available?country=US&area_code=212&limit=5" \
  -H "Authorization: Bearer tine_live_..."
json
{
  "data": [
    {
      "phone_number": "+12125550001",
      "friendly_name": "(212) 555-0001",
      "country_code": "US",
      "region": "NY",
      "capabilities": {
        "sms": true,
        "voice": true,
        "mms": true
      }
    }
  ]
}

Purchase a number

POST

/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.

Request body (JSON)

phone_number

required
stringE.164-formatted number to purchase, e.g. +12125550001. Use GET /v1/numbers/available to find purchasable numbers.

Example

bash
curl -X POST https://tinetech.org/v1/numbers/purchase \
  -H "Authorization: Bearer tine_live_..." \
  -H "Content-Type: application/json" \
  -d '{"phone_number": "+12125550001"}'
json
{
  "data": {
    "phone_number": "+12125550001",
    "country_code": "US",
    "twilio_sid": "PNxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "purchased_at": "2026-09-19T04:50:00.000Z"
  }
}
Messages

List messages

GET

/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.

Query parameters

number

stringE.164 number. Filter to messages sent to or received from this number.

limit

integerMessages to return. Range: 1–100. Default: 50.

before

integerUnix millisecond cursor. Return messages strictly older than this value. Pass next_cursor from the previous response.

Example

bash
curl "https://tinetech.org/v1/messages?number=%2B12125551234&limit=20" \
  -H "Authorization: Bearer tine_live_..."
json
{
  "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

POST

/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.

Request body (JSON)

from

required
stringE.164 sender number. Must be a number owned by your account (see GET /v1/numbers).

to

required
stringE.164 recipient number.

body

stringText content of the message. Required if media_url is not provided.

media_url

string[]Array of publicly accessible https:// URLs to attach as MMS media. Required if body is not provided. Each URL must begin with https://.

Example — SMS

bash
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!"
  }'
json
{
  "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

bash
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

GET

/v1/calls

Returns your call log in reverse-chronological order. Required scope: calls:read.

Query parameters

number

stringE.164 number. Filter to calls to or from this number.

direction

string"inbound" or "outbound".

limit

integerCalls to return. Range: 1–100. Default: 50.

before

stringISO 8601 timestamp cursor. Return calls strictly older than this. Pass next_cursor from the previous response.

Example

bash
curl "https://tinetech.org/v1/calls?direction=outbound&limit=10" \
  -H "Authorization: Bearer tine_live_..."
json
{
  "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

GET

/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.

Path parameters

callSid

required
stringThe call identifier — the id field from GET /v1/calls.

Example

bash
curl https://tinetech.org/v1/calls/CAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/recording \
  -H "Authorization: Bearer tine_live_..."
json
{
  "data": {
    "call_sid": "CAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "recording_url": "https://api.twilio.com/2010-04-01/Accounts/.../Recordings/RE....mp3",
    "recording_duration": 93,
    "has_voicemail": false
  }
}
Voicemails
GET

/v1/voicemails

Returns voicemails in reverse-chronological order. Required scope: calls:read.

Query parameters

limit

integerVoicemails to return. Range: 1–50. Default: 20.

before

stringISO 8601 timestamp cursor. Return voicemails strictly older than this. Pass next_cursor from the previous response.

Example

bash
curl https://tinetech.org/v1/voicemails \
  -H "Authorization: Bearer tine_live_..."
json
{
  "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
GET

/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.

Query parameters

limit

integerTransactions to return. Range: 1–100. Default: 50.

before

stringISO 8601 timestamp cursor. Return transactions strictly older than this. Pass next_cursor from the previous response.

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

bash
curl https://tinetech.org/v1/billing/history \
  -H "Authorization: Bearer tine_live_..."
json
{
  "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
POST

/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

bash
curl -X POST https://tinetech.org/v1/voice/token \
  -H "Authorization: Bearer tine_live_..."
json
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "identity": "dev:uid_abc123",
  "ttl": 3600
}

Using the token (JavaScript)

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.

json
{
  "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.

json
"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).

json
"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).

javascript
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');
}
python
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 True

Retry 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.