Skip to content
API version 0.2.0

API reference

All 2X products share one API. This page is a short reference for developers; the full schema lives in the console and the OpenAPI document.

Status: email sending and webhooks are live. SMS operator connection is launching. OTP endpoints are launching; field names may change.

Conventions

  • Base URL: https://api.reach2x.com
  • Requests and responses are JSON (UTF-8). Times are RFC 3339 in UTC.
  • Phone numbers are E.164 (+905321234567) and masked in responses (+90 5** *** ** 67).
  • Message bodies are kept for your retention period and never returned by the API. For the otp category the body is never stored.

Authentication

Add your project API key as a Bearer token to every request:

Authorization: Bearer rx_canli_… (live) · rx_test_… (test project; never delivered to a real recipient)

Keys are created in the console. The secret is shown once and only its hash is stored. A key belongs to one project and may carry allowed senders, an IP allow list and an expiry.

ScopeAllows
sms.sendSend SMS and cancel scheduled SMS
sms.readRead SMS and the message list
email.sendSend email
email.readRead email
otp.sendSend verification codes (launching)
otp.verifyCheck verification codes (launching)
webhook.manageManage webhook endpoints
balance.readRead quota and usage

Idempotency

Every call that sends something requires an Idempotency-Key header: 8–128 characters from A-Z a-z 0-9 _ - : . (a UUID works). Keys are kept for 24 hours per organization + credential.

  • Same key + same body → the original response, with Idempotent-Replay: true header.
  • Same key + different body → 422 idempotency_key_mismatch.
  • While the first is still running → 409 idempotency_in_progress.

Send email POST /v1/emails

Scope email.send. Creates one message per recipient and returns 202 after storing them durably. The from domain must be verified; messages are DKIM-signed for it. Attachments are not supported yet.

FieldTypeNotes
fromstring | {email, name}Address on your verified sending domain. Required.
tostring | string[]One address or up to 50. Required.
subjectstring1–998 characters. Required.
text / htmlstringAt least one required; each up to 500,000 characters.
categoryinformational | otp | commercialRequired. commercial is not available yet.
reply_tostringReply-to address.
send_atdate-timeUp to 30 days ahead; empty means now.
valid_for_minutesinteger1–4320, default 2880. Then expired.
tagsobjectUp to 10 key/value pairs; no personal data.
curl Send an email
curl https://api.reach2x.com/v1/emails \
  -H "Authorization: Bearer $REACH2X_API_KEY" \
  -H "Idempotency-Key: rapor-2026-11-0001" \
  -H "Content-Type: application/json" \
  -d '{
    "from": { "email": "[email protected]", "name": "Example" },
    "to": "[email protected]",
    "subject": "Aylık raporunuz hazır",
    "text": "Raporunuz hazır: https://example.com/r/Ab12",
    "category": "informational"
  }'
Response202 Accepted
{
  "batch_id": "7d1c2e60-4a8b-4f0e-9b1a-2f6c1d9e3a10",
  "messages": [{
    "id": "c0a8f1e2-5b3d-4e7f-8a9b-1c2d3e4f5a6b",
    "object": "email",
    "status": "queued",
    "category": "informational",
    "from": "[email protected]",
    "to": "a***@example.org"
  }]
}

Read: GET /v1/emails, GET /v1/emails/{id} (with event timeline; scope email.read).

Send SMS POST /v1/sms

The TXT2X operator connection is launching — live soon. You can try the same call today with a test key.

Scope sms.send. category decides the legal handling: informational (transactional), otp (body never stored), commercial (İYS: recipient type and brand code required; recipients without consent fail with recipient_no_consent).

Encoding is automatic: gsm7 160/153, gsm7_tr 155/149, ucs2 70/67 characters per segment.

FieldTypeNotes
tostring | string[]E.164 number or up to 100 distinct numbers. Required.
bodystringAt most 7 segments after encoding. Required.
categoryinformational | otp | commercialRequired. Decides the legal handling.
senderstringApproved sender name, 3–11 chars. Defaults to the project sender.
send_atdate-timeUp to 30 days ahead.
valid_for_minutesinteger1–2880, default 1440.
commercial.recipient_typeindividual | merchantRequired for commercial (İYS recipient type).
tagsobjectUp to 10 key/value pairs.
curl Send an SMS
curl https://api.reach2x.com/v1/sms \
  -H "Authorization: Bearer $REACH2X_API_KEY" \
  -H "Idempotency-Key: randevu-48213" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+905321234567",
    "body": "Randevunuz 18 Kasım 14:30. Değişiklik: https://example.com/r/Ab12",
    "category": "informational",
    "sender": "EXAMPLE"
  }'
Response202 Accepted
{
  "batch_id": "3f9a2b7c-1d4e-4a5b-8c6d-7e8f9a0b1c2d",
  "messages": [{
    "id": "8b7a6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d",
    "object": "sms",
    "status": "queued",
    "to": "+90 5** *** ** 67",
    "sender": "EXAMPLE",
    "segments": 1,
    "encoding": "gsm7_tr"
  }]
}

Other endpoints: GET /v1/sms/{id}, POST /v1/sms/{id}/cancel (scheduled only), GET /v1/balance (scope balance.read).

Verification codes (OTP)

The email channel is live; the SMS channel opens with the operator connection. Every POST needs an Idempotency-Key; one code per recipient per 60 s (at most 5 per hour), the code locks after 5 wrong attempts and is single-use.

  • POST /v1/otp — { channel: "sms" | "email", to, from?, subject?, sender?, length?, ttl_seconds?, template? } → 201 { id, status, attempts_left, expires_at, … }. Scope otp.send.
  • POST /v1/otp/{id}/verify — { code } → { verified, attempts_left, status }; errors otp_expired 410, otp_locked 409, otp_already_verified 409, otp_rate_limited 429. Scope otp.verify.
curl Send a code
curl https://api.reach2x.com/v1/otp \
  -H "Authorization: Bearer $REACH2X_API_KEY" \
  -H "Idempotency-Key: giris-7f3a91c2" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "email",
    "to": "[email protected]",
    "from": "[email protected]",
    "length": 6,
    "ttl_seconds": 300
  }'
Response
{
  "id": "5e4d3c2b-1a09-4f8e-8d7c-6b5a49382716",
  "object": "otp",
  "channel": "email",
  "status": "pending",
  "to": "a***@ornek.com",
  "attempts_left": 5,
  "expires_at": "2026-11-14T09:35:00Z"
}
curl Verify the code
curl https://api.reach2x.com/v1/otp/5e4d3c2b-1a09-4f8e-8d7c-6b5a49382716/verify \
  -H "Authorization: Bearer $REACH2X_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "code": "482913" }'
Response
{
  "verified": true,
  "attempts_left": 4,
  "status": "verified"
}

Message statuses

scheduled → queued → sending → sent → delivered | failed | expired; canceled only from scheduled.

StatusMeaning
scheduledWaiting for send_at; only this status can be canceled.
queuedQueued; also returned to on a temporary network error.
sendingBeing handed to the network (transient).
sentAccepted by the network; waiting for a handset receipt.
deliveredSMS: handset receipt. Email: recipient server accepted (SMTP 250).
failedNot delivered; error.code gives the reason.
expiredValidity ended before delivery.
canceledScheduled message canceled.

Webhook

Manage endpoints with GET/POST /v1/webhooks, DELETE /v1/webhooks/{id}, POST /v1/webhooks/{id}/rotate-secret (scope webhook.manage). Only https URLs resolving to a public address are accepted. The signing secret is shown once, in the create response.

Events: sms.queued, sms.sent, sms.delivered, sms.failed, sms.expired, email.queued, email.delivered, email.failed, email.expired, otp.verified, otp.failed. Use "*" for all.

curl Register a webhook endpoint
curl https://api.reach2x.com/v1/webhooks \
  -H "Authorization: Bearer $REACH2X_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/2x",
    "events": ["sms.delivered", "sms.failed", "email.delivered", "email.failed"]
  }'
Response201 Created
{
  "webhook": {
    "id": "1b2c3d4e-5f60-4a7b-8c9d-0e1f2a3b4c5d",
    "url": "https://example.com/hooks/2x",
    "status": "active",
    "secret_prefix": "whsec_9fK2"
  },
  "secret": "whsec_… (shown only once)"
}

Verify every request

  1. Read header x-imza: t=<unix>,v1=<hex>.
  2. HMAC-SHA256(secret, "<t>.<ham gövde>") (lowercase hex).
  3. Compare with v1 in constant time; reject if |now − t| > 300 s.
  4. x-olay-id deduplicates (the same event may arrive more than once).
webhook.js Verify the signature (Node.js)
import crypto from 'node:crypto';

// header "x-imza": t=<unix seconds>,v1=<hex>
export function verify(rawBody, header, secret) {
  const p = Object.fromEntries(header.split(',').map((x) => x.split('=')));
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${p.t}.${rawBody}`)
    .digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(p.t)) <= 300;
  return (
    fresh &&
    expected.length === p.v1?.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(p.v1))
  );
}

Without a 2xx within 10 s: retries after 1 min, 5 min, 30 min, 2 h, 6 h, 24 h. After 20 consecutive failures the endpoint is disabled. Ordering is not guaranteed; use veri.status and veri.updated_at.

Envelope fields: id, olay (event type), zaman, kiraci (organization), veri (message summary).

Error codes

Errors are RFC 9457 problem+json. code is stable; title and detail are for humans and may change.

application/problem+json Error body example
{
  "type": "https://reach2x.com/docs/errors#sender_not_allowed",
  "title": "Sender not allowed",
  "status": 422,
  "code": "sender_not_allowed",
  "request_id": "req_01JD3Q7Z8K2M"
}
codeHTTPMeaning
invalid_request400Malformed JSON, unknown field or wrong type.
idempotency_key_required400Idempotency-Key missing on a send.
unauthorized401Credential missing, invalid, expired or revoked.
forbidden403Missing scope, sender or IP permission.
not_found404Not in your organization (other tenants’ ids too).
idempotency_in_progress409Same-key request still in progress.
not_cancelable409Message is no longer scheduled.
payload_too_large413Body larger than 1 MB.
idempotency_key_mismatch422Same key reused with a different body.
validation_failed422Semantically invalid input; errors[] lists fields.
invalid_phone_number422Recipient is not valid E.164.
body_too_long422Body needs more than 7 segments.
sender_not_allowed422Sender not approved or not allowed for this key.
iys_recipient_type_required422commercial without recipient_type.
iys_brand_code_missing422commercial but no İYS brand code.
invalid_send_at422send_at in the past or beyond 30 days.
sender_domain_not_verified422from domain not verified (email).
commercial_email_not_supported422commercial email not available yet.
quota_exceeded429Monthly segment quota reached (informational/OTP continue).
rate_limited429Too many requests; honor Retry-After.
internal_error500Unexpected error; report with request_id.
service_unavailable503Temporary; safe to retry with the same Idempotency-Key.

Delivery failures (error.code — SMS): undeliverable, unknown_number, rejected_by_carrier, content_rejected, duplicate_message, recipient_no_consent, commercial_not_permitted, recipient_blocked, destination_not_supported, sender_rejected, expired, delivery_failed, delivery_unavailable.

Email: recipient_rejected, domain_not_found, message_rejected, delivery_unavailable, expired, delivery_failed.

Reading and pagination

Lists are newest first and cursor based: limit (1–100, default 50) + cursor → { data, next_cursor, has_more }. Filters: status, category, batch_id, created_after, created_before.

Start free