Complete LLM & Developer API Reference (/llms-full.txt)
# BlastWave SMS — Complete LLM & Developer API Reference (`v1`)
> **Machine-Readable LLM Entrypoint**: Point any AI coding agent (Claude Code, Cursor, Codex, Windsurf, OpenAI, Gemini) at `https://sms.reliadata.io/llms-full.txt` or `https://sms.reliadata.io/api/v1/docs/openapi.json` to give it complete, zero-hallucination knowledge of every BlastWave SMS API endpoint, request/response schema, compliance workflow, AI agent configuration, and HMAC-SHA256 webhook contract.
---
## Table of Contents
1. [System Prompt & Core Platform Invariants](#1-system-prompt--core-platform-invariants)
2. [Onboarding, Authentication, MFA, Organization & Team Management](#2-onboarding-authentication-mfa-organization--team-management)
3. [Scoped API Keys](#3-scoped-api-keys)
4. [Billing, Prepaid Credits, Subscriptions & Auto-Recharge](#4-billing-prepaid-credits-subscriptions--auto-recharge)
5. [Getting New Phone Numbers: Search, Order, Configure & Link DIDs](#5-getting-new-phone-numbers-search-order-configure--link-dids)
6. [Carrier Compliance: 10DLC Brands, Vetting, Campaigns, Mini-Sites, Toll-Free (TFV), Opt-Outs & Quiet Hours](#6-carrier-compliance-10dlc-brands-vetting-campaigns-mini-sites-toll-free-tfv-opt-outs--quiet-hours)
7. [Contacts, Custom Fields, Tags, Smart Lists & File Imports](#7-contacts-custom-fields-tags-smart-lists--file-imports)
8. [Reusable Message Templates, Merge Tags, Filters & Spintax](#8-reusable-message-templates-merge-tags-filters--spintax)
9. [Sending Transactional & Bulk SMS/MMS, Media Uploads & Short Links](#9-sending-transactional--bulk-smsmms-media-uploads--short-links)
10. [Broadcast, A/B Test, Number-Pool & Personalized CSV-Merge Campaigns](#10-broadcast-ab-test-number-pool--personalized-csv-merge-campaigns)
11. [Two-Way Inbox, Conversations, Operator Assignment & Real-Time WebSockets](#11-two-way-inbox-conversations-operator-assignment--real-time-websockets)
12. [Automations: Keyword Auto-Replies & Multi-Step Drip Sequences](#12-automations-keyword-auto-replies--multi-step-drip-sequences)
13. [Conversational AI Agents & UniRoma+ AI Copywriting Suite](#13-conversational-ai-agents--uniroma-ai-copywriting-suite)
14. [Outbound Customer Webhooks (All 8 Events + HMAC-SHA256 Verification)](#14-outbound-customer-webhooks-all-8-events--hmac-sha256-verification)
15. [Analytics, Carrier Error Diagnostics & CSV Exports](#15-analytics-carrier-error-diagnostics--csv-exports)
16. [Complete End-to-End SDK Clients (Python & TypeScript)](#16-complete-end-to-end-sdk-clients-python--typescript)
---
## 1. System Prompt & Core Platform Invariants
Copy-paste or reference these invariants when generating integration code:
1. **Base URLs**:
- REST API Base URL: `https://sms.reliadata.io/api/v1`
- Service Health Check: `GET /health` (`https://sms.reliadata.io/health` -> `{"status": "ok", "service": "blastwave-sms"}`)
- LLM Index & Full Reference: `GET /llms.txt`, `GET /api/v1/docs/llms.txt`, `GET /llms-full.txt`, `GET /api/v1/docs/llms-full.txt`, `GET /api/v1/docs/llm`
- Real-Time WebSocket URL: `wss://sms.reliadata.io/ws?token=<jwt_access_token>`
- Public Short-Link Redirects: `https://sms.reliadata.io/l/{code}`
- Public 10DLC Compliance Mini-Sites: `https://sms.reliadata.io/websites/{brand_slug}/{token}`
- Live OpenAPI 3.1 Spec: `https://sms.reliadata.io/api/v1/docs/openapi.json`
2. **Authentication**:
- **Server-to-Server API Keys (Recommended)**: Pass `X-API-Key: bw_live_<64_hex_chars>` on every request, or `Authorization: Bearer bw_live_<64_hex_chars>`.
- **User Sessions / Browser SPA**: Authenticate via `POST /api/v1/auth/login` and pass `Authorization: Bearer <jwt_access_token>`.
3. **Phone Number Normalization (E.164)**:
- All phone numbers are stored and returned in strict **E.164 format** (`+13075550199`).
- Input endpoints accept 10-digit US/Canada numbers (`3075550199` or `(307) 555-0199`) and automatically normalize them to `+13075550199`.
4. **URL Trailing-Slash Normalization**:
- The ASGI middleware automatically normalizes trailing slashes (`/api/v1/contacts` and `/api/v1/contacts/` behave identically without `307` redirects).
5. **Pricing, Segments & Wallet Credits**:
- Outbound SMS: **`$0.0080` per segment** (`160` chars GSM-7 single segment, `153` chars per multipart GSM-7 segment; `70`/`67` chars UCS-2 Unicode when emojis or non-GSM characters are present).
- Outbound MMS (whenever `media_urls` is non-empty): **`$0.0200` per message** (up to 1.5 MB image payload).
- Local 10DLC or Toll-Free DID: **`$1.00` upon order** and **`$1.00/month`** recurring on the 1st of the month.
- **Automatic Carrier Failure Refunds**: If Bandwidth reports a terminal carrier failure DLR on an outbound message, BlastWave automatically refunds the deducted credit and logs a `credit_refund` billing event.
- If a tenant's wallet balance is insufficient for an outbound message or number order, the API returns **`402 Payment Required`**.
6. **TCPA & Opt-Out Guardrails**:
- Inbound `STOP`, `STOPALL`, `UNSUBSCRIBE`, `CANCEL`, `END`, `QUIT` (and natural-language opt-outs such as *"take me off your list"*) immediately mark the contact `opted_out` and fire `contact.opted_out`.
- Any attempt to send to an opted-out recipient via `/messages/send`, `/inbox/{id}/reply`, `/campaigns/{id}/launch`, or automations is blocked automatically (`400 Bad Request` on 1:1 sends; skipped on campaigns/drips).
- Inbound `START`, `UNSTOP`, `YES` from the handset — or `DELETE /api/v1/compliance/opt-outs?phone_number=+1...` — clears the opt-out and fires `contact.opted_in`.
### HTTP Status Codes
| Status | Meaning | Typical Cause |
| :--- | :--- | :--- |
| `200 OK` | Request succeeded | Standard `GET`, `PUT`, `DELETE`, or action endpoint |
| `201 Created` | Resource created | `POST` to create contact, message, campaign, number order, AI agent, etc. |
| `400 Bad Request` | Business rule violation | Opted-out recipient, invalid E.164 phone, 49-DID 10DLC campaign limit reached |
| `401 Unauthorized` | Missing/invalid auth | Expired JWT or revoked/invalid `bw_live_...` API key |
| `402 Payment Required` | Insufficient credits | `credit_balance` is below the cost of the message, campaign, or DID order |
| `403 Forbidden` | Insufficient permissions | API key lacks required scope (e.g., missing `messages:send`) or `viewer` role attempting write |
| `404 Not Found` | Resource not found | UUID does not exist or belongs to a different tenant |
| `409 Conflict` | Duplicate active resource | Active contact with same phone number already exists in tenant |
| `413 Payload Too Large` | Upload limit exceeded | MMS image > 1.5 MB or CSV/Excel merge file > 25 MB |
| `422 Unprocessable Entity` | Schema validation error | Missing required JSON field or invalid enum value |
| `429 Too Many Requests` | Rate limit exceeded | Exceeded per-API-key (`60 req/min` default) or auth rate limit |
| `502 Bad Gateway` | Upstream carrier error | Bandwidth or Stripe returned an upstream error |
---
## 2. Onboarding, Authentication, MFA, Organization & Team Management
### 2.1 Register a New Tenant Account
`POST /api/v1/auth/register` (`201 Created`)
Creates a new tenant organization, owner user account, and returns JWT tokens.
```json
{
"email": "founder@acmesolar.com",
"password": "StrongPassword!2026",
"first_name": "Alex",
"last_name": "Rivera",
"company_name": "Acme Solar LLC"
}
```
**Response (`201 Created`):**
```json
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer",
"user": {
"id": "5f8c3d01-1234-4a5b-8c9d-0123456789ab",
"email": "founder@acmesolar.com",
"first_name": "Alex",
"last_name": "Rivera",
"role": "owner",
"tenant_id": "9a1b2c3d-4567-89ab-cdef-0123456789ab",
"mfa_enabled": false,
"is_superadmin": false
}
}
```
### 2.2 Login & Multi-Factor Authentication (MFA)
- **`POST /api/v1/auth/login`**:
```json
{
"email": "founder@acmesolar.com",
"password": "StrongPassword!2026"
}
```
If MFA is enabled on the user account, returns `{"mfa_required": true, "mfa_token": "<temp_token>"}`. Complete login via:
- **`POST /api/v1/auth/mfa/validate`**:
```json
{
"mfa_token": "<temp_token>",
"code": "123456"
}
```
- **Refresh JWT**: `POST /api/v1/auth/refresh` with `{"refresh_token": "..."}`.
- **Logout Session**: `POST /api/v1/auth/logout` (clears session cookies and invalidates the active session).
- **Mint Onboarding FCR Email**: `POST /api/v1/auth/fcr-email` (generates a fresh catch-all verification email address during account registration).
- **Current User Profile**: `GET /api/v1/auth/me`.
- **Setup / Verify / Disable TOTP MFA**:
- `POST /api/v1/auth/mfa/setup` -> returns `{"secret": "...", "qr_code_uri": "otpauth://totp/..."}`
- `POST /api/v1/auth/mfa/verify` -> `{"code": "123456"}`
- `POST /api/v1/auth/mfa/disable` -> `{"code": "123456"}`
- **Password Management**:
- `POST /api/v1/auth/change-password` -> `{"current_password": "...", "new_password": "..."}`
- `POST /api/v1/auth/password-reset/request` -> `{"email": "founder@acmesolar.com"}`
- `POST /api/v1/auth/password-reset/confirm` -> `{"token": "...", "new_password": "..."}`
### 2.3 Organization Settings & Team Invites (`/api/v1/tenants/current`)
- **`GET /api/v1/tenants/current`**: Returns organization details (`id`, `name`, `slug`, `plan_tier`, `credit_balance`, `status`, `settings`).
- **`PUT /api/v1/tenants/current`**: Update organization name or settings (`{"name": "Acme Solar Holdings", "settings": {"timezone": "America/Chicago"}}`).
- **`GET /api/v1/tenants/current/usage`**: Returns active counts (`users`, `phone_numbers`, `contacts`, `campaigns`, `credit_balance`).
- **Team Members & Invites**:
- `GET /api/v1/tenants/current/users`: List all users in the tenant.
- `POST /api/v1/tenants/current/invites` (`201 Created`): Send an email invite or generate an invite link:
```json
{
"email": "operator@acmesolar.com",
"role": "sender",
"first_name": "Jordan",
"last_name": "Lee"
}
```
*(Valid roles: `"owner"`, `"admin"`, `"sender"`, `"viewer"`.)*
- `GET /api/v1/auth/invite-info?token=<invite_token>` & `POST /api/v1/auth/accept-invite`: Accept a team invitation.
- `PUT /api/v1/tenants/current/users/{user_id}`: Update role (`admin`, `sender`, `viewer`) or `is_active`.
- `DELETE /api/v1/tenants/current/users/{user_id}`: Deactivate a team member.
---
## 3. Scoped API Keys
Create and manage `bw_...` API keys for programmatic integrations. Leaving `scopes` empty (`[]`) or passing `["*"]` grants full tenant access across all endpoints.
### 3.1 Available Scopes
| Scope | Permitted Operations |
| :--- | :--- |
| `*` (or `[]`) | Full access to all tenant endpoints |
| `messages:send` / `messages:write` | `POST /api/v1/messages/send`, `POST /api/v1/messages/send-bulk`, `POST /api/v1/inbox/{id}/reply`, `POST /api/v1/media/upload`, `POST /api/v1/ai/*` |
| `messages:read` | `GET /api/v1/messages*`, `GET /api/v1/inbox*`, `GET /api/v1/media/*` |
| `contacts:read` | `GET /api/v1/contacts*`, `POST /api/v1/contacts/parse-file`, `GET /api/v1/lists*`, `POST /api/v1/lists/smart-preview` |
| `contacts:write` | `POST/PUT/DELETE /api/v1/contacts*`, `POST/PUT/DELETE /api/v1/lists*` |
| `campaigns:read` / `templates:read` | `GET /api/v1/campaigns*`, `GET /api/v1/templates*`, `POST /api/v1/templates/inspect`, `POST /api/v1/templates/{id}/preview` |
| `campaigns:write` / `templates:write` | `POST/PUT/DELETE /api/v1/campaigns*`, `POST/PUT/DELETE /api/v1/templates*` |
| `numbers:read` | `GET /api/v1/numbers*`, `POST /api/v1/numbers/search` |
| `numbers:write` | `POST /api/v1/numbers/order`, `PUT/DELETE /api/v1/numbers/{id}`, `POST /api/v1/numbers/sync` |
| `compliance:read` | `GET /api/v1/compliance*`, `POST /api/v1/compliance/campaigns/preflight-check` |
| `compliance:write` | `POST/PUT/DELETE /api/v1/compliance*` |
| `automations:read` / `ai-agents:read` | `GET /api/v1/automations*`, `GET /api/v1/ai-agents*` |
| `automations:write` / `ai-agents:write` | `POST/PUT/DELETE /api/v1/automations*`, `POST/PUT/DELETE /api/v1/ai-agents*` |
| `billing:read` / `billing:write` | `GET /api/v1/billing*`, `POST/PUT /api/v1/billing*` |
| `analytics:read` | `GET /api/v1/analytics*` |
| `webhooks:manage` / `webhooks:write` | `GET/POST/PUT/DELETE /api/v1/webhooks/endpoints*` and `/api/v1/tenants/current/webhooks*` |
| `admin:read` / `admin:write` | `GET/PUT /api/v1/tenants/current*`, `GET/POST/PUT/DELETE /api/v1/api-keys*` |
### 3.2 Endpoints
- **`POST /api/v1/api-keys`** (`201 Created` — requires `owner` or `admin` role):
```json
{
"name": "Production CRM Integration",
"scopes": ["*"],
"expires_at": null
}
```
**Response (`201 Created`):**
```json
{
"id": "b12c34d5-e6f7-4a8b-9c0d-1e2f3a4b5c6d",
"name": "Production CRM Integration",
"key_prefix": "bw_xK9pQ",
"scopes": ["*"],
"is_active": true,
"last_used_at": null,
"expires_at": null,
"created_at": "2026-11-01T22:10:00+00:00",
"api_key": "bw_xK9pQ2mN8vL4rT6wY1zB3cV5nM7qP9sD0fG2hJ4kL6",
"raw_key": "bw_xK9pQ2mN8vL4rT6wY1zB3cV5nM7qP9sD0fG2hJ4kL6"
}
```
*(Both `api_key` and `raw_key` contain the full plaintext secret **once** at creation time. Store it securely.)*
- **`GET /api/v1/api-keys`**: List all API keys (`id`, `name`, `key_prefix`, `scopes`, `last_used_at`, `is_active`).
- **`GET /api/v1/api-keys/{key_id}`**: Retrieve metadata for a single API key.
- **`PUT /api/v1/api-keys/{key_id}`**: Update `name`, `scopes`, or `is_active`.
- **`DELETE /api/v1/api-keys/{key_id}`**: Revoke an API key immediately.
- **`GET /api/v1/api-keys/{key_id}/usage`**: Usage statistics for the key.
---
## 4. Billing, Prepaid Credits, Subscriptions & Auto-Recharge
### 4.1 Check Wallet Balance & Plan Usage
`GET /api/v1/billing` (or `GET /api/v1/billing/usage`)
**Response (`200 OK`):**
```json
{
"plan_tier": "growth",
"plan_name": "Growth",
"plan_price": 149.0,
"credit_balance": 245.52,
"current_period_sms": 3420,
"current_period_mms": 115,
"included_sms": 5000,
"included_mms": 500,
"overage_cost": 30.28,
"next_invoice_date": "2026-12-01T00:00:00+00:00",
"stripe_customer_id": "cus_...",
"stripe_subscription_id": "sub_...",
"auto_recharge": {
"enabled": true,
"threshold": 25.0,
"amount": 100.0,
"low_balance_alert_threshold": 50.0
}
}
```
### 4.2 Configure Wallet Auto-Recharge
`PUT /api/v1/billing/auto-recharge`
Automatically tops up the tenant's prepaid wallet via their saved Stripe payment method whenever `credit_balance` drops below `threshold`:
```json
{
"enabled": true,
"threshold": 25.0,
"amount": 100.0,
"low_balance_alert_threshold": 50.0
}
```
### 4.3 Purchase Credits, Manage Subscriptions & Export Ledger
- **`GET /api/v1/billing/plans`**: List all available subscription tiers (`free_trial`, `starter`, `growth`, `enterprise`) and their limits (`max_numbers`, `max_users`, `max_contacts`, `max_ai_agents`).
- **`POST /api/v1/billing/credits/purchase`**: Create a Stripe Checkout session to buy prepaid credits (`$5.00` to `$10,000.00`):
```json
{ "amount": 100.00 }
```
Returns `{"checkout_url": "https://checkout.stripe.com/...", "session_id": "cs_...", "amount": 100.0}`.
- **`POST /api/v1/billing/checkout`**: Subscribe to a plan (`{"plan": "starter" | "growth" | "enterprise"}`).
- **`POST /api/v1/billing/portal`**: Generate a Stripe Billing Portal URL (`{"portal_url": "https://billing.stripe.com/..."}`).
- **`GET /api/v1/billing/invoices`**: List Stripe invoices with PDF download links.
- **`GET /api/v1/billing/events?page=1&per_page=50&event_type=sms_sent`**: Paginated ledger (`sms_sent`, `mms_sent`, `credit_refund`, `number_purchase`, `number_renewal`, `credit_purchase`).
- **`GET /api/v1/billing/events/export?limit=2000`**: Download billing ledger as CSV.
---
## 5. Getting New Phone Numbers: Search, Order, Configure & Link DIDs
Every outbound SMS/MMS or AI agent requires at least one active phone number (`local` 10DLC or `toll_free`).
### 5.1 Step 1 — Search Available Numbers on Bandwidth
`POST /api/v1/numbers/search`
> **Important**: `/api/v1/numbers/search` is a **`POST`** request taking a JSON body.
**Request Body:**
```json
{
"area_code": "307",
"state": "WY",
"city": null,
"zip_code": null,
"quantity": 10,
"number_type": "local"
}
```
*(Set `"number_type": "toll_free"` to search `800`/`888`/`877`/`866`/`855`/`844`/`833` Toll-Free numbers.)*
**Response (`200 OK`):**
```json
{
"available_numbers": [
{
"number": "+13075550140",
"city": "CHEYENNE",
"state": "WY",
"rate_center": "CHEYENNE",
"region": "WY",
"type": "local",
"capabilities": ["sms", "mms"],
"monthly_cost": 1.0
}
]
}
```
### 5.2 Step 2 — Order & Provision Phone Numbers
`POST /api/v1/numbers/order` (`201 Created`)
Purchases the requested numbers on Bandwidth (`$1.00/number` deducted from wallet credits), creates `PhoneNumber` records in your tenant, and optionally links them to an approved 10DLC campaign in a single call.
> **10DLC Standard Campaign Limit**: Standard TCR 10DLC campaigns permit at most **49 phone numbers** per campaign unless the campaign was registered with `number_pool: true`.
**Request Body:**
```json
{
"numbers": ["+13075550140", "+13075550141"],
"campaign_id": "d29f8b2a-9a14-48d2-8a5c-71a0e9234b11"
}
```
*(Omit `campaign_id` or pass `null` if ordering Toll-Free numbers or if you plan to link a 10DLC campaign later.)*
**Response (`201 Created`):**
```json
{
"ordered": 2,
"order_id": "bw-order-981273",
"tn_option_order_id": "bw-tnopt-44129",
"numbers": [
{
"id": "a63e70e4-0a5b-4a8e-9b29-52028632056a",
"number": "+13075550140",
"friendly_name": null,
"number_type": "local",
"capabilities": ["sms", "mms"],
"status": "active",
"campaign_id": "d29f8b2a-9a14-48d2-8a5c-71a0e9234b11",
"monthly_cost": 1.0,
"created_at": "2026-11-01T22:15:00+00:00"
}
]
}
```
### 5.3 Step 3 — List, Configure, Link 10DLC Campaigns & Release Numbers
- **`GET /api/v1/numbers`**:
- Query params: `page=1`, `per_page=50`, `number_type=local|toll_free`, `status=active`.
- Returns `{"numbers": [...], "total": N, "page": 1, "per_page": 50}` including `campaign_name`, `bandwidth_campaign_id`, and `campaign_use_case`.
- **`PUT /api/v1/numbers/{number_id}`** (also accepts `PATCH`):
Update a number's friendly label or assign/unassign a 10DLC campaign (automatically submits the Bandwidth `TnOptionOrder` SMS Campaign ID assignment when the campaign is `ACTIVE`/`APPROVED`):
```json
{
"friendly_name": "Main Sales & AI Concierge Line",
"campaign_id": "d29f8b2a-9a14-48d2-8a5c-71a0e9234b11",
"unassign_campaign": false
}
```
- **`GET /api/v1/numbers/{number_id}/stats`**:
Returns lifetime delivery counters for that DID: `{"sent": 1420, "delivered": 1398, "failed": 22}`.
- **`POST /api/v1/numbers/sync`**:
Reconciles tenant DID statuses and 10DLC campaign assignments with Bandwidth.
- **`DELETE /api/v1/numbers/{number_id}`**:
Unassigns any 10DLC campaign on Bandwidth, disconnects the DID, and marks it `"released"`.
---
## 6. Carrier Compliance: 10DLC Brands, Vetting, Campaigns, Mini-Sites, Toll-Free (TFV), Opt-Outs & Quiet Hours
US carriers require **10DLC registration** for local numbers and **Toll-Free Verification (TFV)** for `8XX` numbers. BlastWave automates the entire lifecycle, including AI company research, automatic CTIA-compliant Mini-Site generation, preflight DCA2 checks, and AI campaign generation.
### 6.1 Step 1 — AI Company Research (Optional Pre-Fill from Legal Name + EIN)
`POST /api/v1/compliance/brands/ai-research`
Researches SEC EDGAR, IRS Form 990, official website JSON-LD, and public records to pre-populate 10DLC Brand fields and optionally mint a fresh FCR verification email:
```json
{
"legal_name": "Acme Solar LLC",
"ein": "84-1234567",
"dba_name": "Acme Solar",
"website_hint": "https://acmesolar.com",
"state_hint": "TX",
"use_fcr_email": true
}
```
### 6.2 Step 2 — Register a 10DLC Brand
`POST /api/v1/compliance/brands/apply` (`201 Created`)
> **Valid `entity_type` Values**: `PRIVATE_PROFIT`, `PUBLIC_PROFIT`, `NON_PROFIT`, `GOVERNMENT`, `SOLE_PROPRIETOR`
> **Valid Bandwidth `vertical` Values (17 total)**: `REAL_ESTATE`, `HEALTHCARE`, `ENERGY`, `ENTERTAINMENT`, `RETAIL`, `AGRICULTURE`, `INSURANCE`, `EDUCATION`, `HOSPITALITY`, `FINANCIAL`, `GAMBLING`, `CONSTRUCTION`, `NGO`, `MANUFACTURING`, `GOVERNMENT`, `TECHNOLOGY`, `COMMUNICATION`. *(Legacy aliases like `PROFESSIONAL` or `LEGAL` are automatically normalized.)*
```json
{
"entity_type": "PRIVATE_PROFIT",
"legal_name": "Acme Solar LLC",
"dba_name": "Acme Solar",
"ein": "84-1234567",
"phone": "+13075550140",
"email": "compliance@acmesolar.com",
"street": "500 Congress Ave Ste 1400",
"city": "Austin",
"state": "TX",
"zip_code": "78701",
"country": "US",
"website": "https://acmesolar.com",
"vertical": "ENERGY",
"description": "Residential solar installation, maintenance, and customer financing.",
"brand_relationship": "SMALL_ACCOUNT",
"auto_create_minisite": true
}
```
*(Setting `"auto_create_minisite": true` automatically generates and publishes a hosted CTIA/DCA2-compliant opt-in website, Privacy Policy, and Terms of Service for the brand!)*
- **List Brands**: `GET /api/v1/compliance/brands`
- **Get Brand Status**: `GET /api/v1/compliance/brands/{brand_id}` (`identity_status: "VERIFIED"`, `vetting_status`, `trust_score`, `registration_status`)
- **List Brand External Vettings**: `GET /api/v1/compliance/brands/{brand_id}/vettings`
- **Request 3rd-Party External Vetting (AEGIS / Campaign Verify)**:
`POST /api/v1/compliance/brands/{brand_id}/vet`
```json
{ "evp_id": "AEGIS", "vetting_class": "STANDARD" }
```
- **Import Existing Vetting Token**:
`POST /api/v1/compliance/brands/{brand_id}/vetting-import`
```json
{ "evp_id": "CV", "vetting_id": "cv-vet-99182", "vetting_token": "tok_..." }
```
- **Provision & Poll FCR Verification Emails**:
- `POST /api/v1/compliance/fcr/emails`: `{"count": 1, "ai": true}` (mints unique FCR catch-all email addresses for brand/caller registry verification)
- `GET /api/v1/compliance/fcr/codes?email=<fcr_email>`: Polls inbound verification codes delivered to a tenant's FCR email address.
### 6.3 Step 3 — Generate or Customize a 10DLC Compliance Mini-Site
`POST /api/v1/compliance/minisites` (`201 Created`)
If a brand does not have a compliant opt-in web form, Privacy Policy, and SMS Terms & Conditions on its main site, create a hosted BlastWave Compliance Mini-Site:
```json
{
"brand_id": "7f3c9a12-4567-4b89-9123-abcdef012345",
"brand_name": "Acme Solar",
"legal_name": "Acme Solar LLC",
"vertical": "ENERGY",
"company_description": "Residential solar installation and monitoring services.",
"services_summary": "Appointment reminders, installation updates, and solar savings offers.",
"use_cases": ["CUSTOMER_CARE", "MARKETING"],
"include_marketing_consent": true,
"accent_color": "#2563eb",
"street": "500 Congress Ave Ste 1400",
"city": "Austin",
"state": "TX",
"zip_code": "78701",
"country": "US",
"support_email": "support@acmesolar.com",
"support_phone": "+13075550140",
"update_brand_website": true
}
```
Returns `site_url`, `privacy_policy_url`, `terms_and_conditions_url`, and `recommended_campaign_fields` ready to pass directly into 10DLC campaign registration.
- **List / Update / Delete Mini-Sites**: `GET /api/v1/compliance/minisites`, `PUT /api/v1/compliance/minisites/{minisite_id}`, `DELETE /api/v1/compliance/minisites/{minisite_id}`.
- **Public Visitor Opt-In Submission**: `POST /api/v1/public/websites/{brand_slug}/{token}/opt-in` (automatically creates/opts in the `Contact` and logs a TCPA consent record).
### 6.4 Step 4 — AI-Generate, Preflight-Check & Register a 10DLC Campaign
1. **AI-Generate Carrier-Compliant Campaign Copy**:
`POST /api/v1/compliance/campaigns/ai-generate`
```json
{
"brand_id": "7f3c9a12-4567-4b89-9123-abcdef012345",
"use_case": "MIXED",
"sub_usecases": ["CUSTOMER_CARE", "MARKETING"],
"business_goal": "Send solar installation appointment confirmations, status alerts, and seasonal promotions.",
"auto_ensure_minisite": true
}
```
Returns complete, DCA2-compliant `description`, `message_flow`, `sample_messages`, `help_message`, `optin_message`, `optout_message`, `privacy_policy_link`, and `terms_and_conditions_link`.
2. **Run Preflight DCA2 Readiness Check**:
`POST /api/v1/compliance/campaigns/preflight-check`
Verifies live HTTP reachability of `opt_in_url`, `privacy_policy_link`, and `terms_and_conditions_link`, non-sharing mobile data disclosures, brand name alignment, and sample message keyword requirements. Returns `{"passed": true, "score": 100, "checks": [...], "blocking_issues": []}`.
3. **Submit 10DLC Campaign Application**:
`POST /api/v1/compliance/campaigns/apply` (`201 Created`)
> **Valid `use_case` Values**: `MIXED`, `LOW_VOLUME`, `CUSTOMER_CARE`, `MARKETING`, `ACCOUNT_NOTIFICATION`, `DELIVERY_NOTIFICATION`, `2FA`, `FRAUD_ALERT`, `SECURITY_ALERT`, `HIGHER_EDUCATION`, `K12_EDUCATION`, `POLLING_VOTING`, `PUBLIC_SERVICE_ANNOUNCEMENT`, `SOLE_PROPRIETOR`, `CHARITY`, `EMERGENCY`, `POLITICAL`, `SWEEPSTAKE`, `AGENTS_FRANCHISES`. *(When `use_case` is `MIXED` or `LOW_VOLUME`, provide 2–5 `sub_usecases` and at least 2 `sample_messages`.)*
```json
{
"brand_id": "7f3c9a12-4567-4b89-9123-abcdef012345",
"use_case": "MIXED",
"sub_usecases": ["CUSTOMER_CARE", "MARKETING"],
"description": "Acme Solar sends appointment reminders, installation updates, and promotional solar consultations to customers who opted in via our website.",
"message_flow": "Customers opt in by filling out the web form at https://sms.reliadata.io/websites/acme-solar/x9k2p1 and checking the optional SMS consent checkbox, or by texting START to (307) 555-0140.",
"sample_messages": [
"Acme Solar: Hi {{first_name}}, your roof inspection is confirmed for {{date}} at 10:00 AM. Reply HELP for help or STOP to opt out.",
"Acme Solar: Save 20% on battery backup installation this month! Visit https://sms.reliadata.io/websites/acme-solar/x9k2p1. Reply STOP to opt out."
],
"help_message": "Acme Solar Support: Call (307) 555-0140 or email support@acmesolar.com for assistance. Msg&data rates may apply. Reply STOP to opt out.",
"help_keywords": "HELP,INFO,SUPPORT",
"optin_message": "Acme Solar: You are now subscribed to SMS updates. Msg frequency varies. Msg&data rates may apply. Reply HELP for help, STOP to opt out.",
"optin_keywords": "START,YES,UNSTOP",
"optout_message": "Acme Solar: You have been unsubscribed and will receive no further messages. Reply START to resubscribe.",
"optout_keywords": "STOP,UNSUBSCRIBE,CANCEL,END,QUIT",
"subscriber_optin": true,
"subscriber_optout": true,
"subscriber_help": true,
"embedded_links": true,
"embedded_phone": true,
"number_pool": false,
"privacy_policy_link": "https://sms.reliadata.io/websites/acme-solar/x9k2p1/privacy",
"terms_and_conditions_link": "https://sms.reliadata.io/websites/acme-solar/x9k2p1/terms",
"phone_number_ids": ["a63e70e4-0a5b-4a8e-9b29-52028632056a"]
}
```
- **List / Inspect 10DLC Campaigns**: `GET /api/v1/compliance/campaigns`, `GET /api/v1/compliance/campaigns/{campaign_id}`, `DELETE /api/v1/compliance/campaigns/{campaign_id}`
- **Track 10DLC Application Queue**:
- `GET /api/v1/compliance/applications`
- `GET /api/v1/compliance/applications/{application_id}`
- `GET /api/v1/compliance/applications/{application_id}/review-status`
- **Auto-Heal Rejected Campaigns & Re-Vet**:
- `POST /api/v1/compliance/campaigns/{campaign_id}/auto-heal`: `{"apply_changes": true, "submit_revet": true}`
- `POST /api/v1/compliance/campaigns/{campaign_id}/revet`
- **Sync Live Bandwidth 10DLC State**: `POST /api/v1/compliance/sync-bandwidth`
- **Compliance Dashboard & Audit Trail**:
- `GET /api/v1/compliance/dashboard`: Returns `total_brands`, `total_campaigns`, `active_numbers`, `opt_out_count_30d`, `pending_brands`, `pending_campaigns`, and `trust_score`.
- `GET /api/v1/compliance/audit-log?page=1&per_page=50`: Immutable compliance audit log.
### 6.5 Step 5 — Toll-Free Verification (TFV)
For `800`/`888`/`877`/`866`/`855`/`844`/`833` numbers:
- **`GET /api/v1/compliance/toll-free`**: List all tenant Toll-Free numbers and their verification statuses.
- **`POST /api/v1/compliance/toll-free/verify`** (`201 Created`):
```json
{
"phone_number_id": "b72e81f5-1b6c-4b9f-8c30-63139743167b",
"business_name": "Acme Solar LLC",
"corporate_website": "https://acmesolar.com",
"contact_first_name": "Alex",
"contact_last_name": "Rivera",
"contact_email": "compliance@acmesolar.com",
"contact_phone": "+13075550140",
"street": "500 Congress Ave Ste 1400",
"city": "Austin",
"state": "TX",
"zip_code": "78701",
"use_case": "CUSTOMER_CARE",
"use_case_summary": "Customer support replies and solar installation scheduling confirmations.",
"opt_in_workflow_description": "Customers opt in on our website contact form at https://acmesolar.com/contact by checking the explicit SMS consent checkbox.",
"opt_in_image_urls": ["https://acmesolar.com/assets/optin-form.png"],
"sample_message": "Acme Solar: Hi Jane, your technician is 15 minutes away. Reply HELP for help or STOP to opt out.",
"monthly_volume": "10,000"
}
```
### 6.6 Step 6 — Opt-Out Ledger, TCPA Consent & Quiet Hours
- **Opt-Outs**:
- `GET /api/v1/compliance/opt-outs?page=1&per_page=50&search=+1307`
- `POST /api/v1/compliance/opt-outs`: `{"phone_number": "+13075550188", "reason": "Requested via support call"}` (fires `contact.opted_out` webhook)
- `DELETE /api/v1/compliance/opt-outs?phone_number=%2B13075550188`: Reactivates an opted-out phone number and fires `contact.opted_in` webhook
- **TCPA Consent Records**:
- `GET /api/v1/compliance/tcpa-consent?page=1&per_page=50&phone_number=+1307...`
- `POST /api/v1/compliance/tcpa-consent`: `{"phone_number": "+13075550188", "consent_type": "express_written", "source": "web_checkout", "ip_address": "203.0.113.42"}`
- **Quiet Hours Enforcement**:
- `GET /api/v1/compliance/quiet-hours`
- `PUT /api/v1/compliance/quiet-hours`:
```json
{
"enabled": true,
"start": "21:00",
"end": "08:00",
"timezone": "US/Eastern"
}
```
---
## 7. Contacts, Custom Fields, Tags, Smart Lists & File Imports
### 7.1 Create or Reactivate a Contact
`POST /api/v1/contacts` (`201 Created`)
> **Soft-Delete Reactivation**: If a contact with the same phone number was previously deleted (`status="deleted"`), `POST /api/v1/contacts` automatically reactivates the record with the new fields. If the contact previously opted out via `STOP`, their `opted_out` status is preserved until explicitly opted back in.
```json
{
"phone_number": "+13075550199",
"first_name": "Jane",
"last_name": "Doe",
"email": "jane@example.com",
"custom_fields": {
"city": "Cheyenne",
"roof_type": "Composite Shingle",
"utility_bill": "$240",
"tags": ["vip", "solar-lead"]
},
"opt_in_method": "web_form",
"list_ids": ["3d6f0a8e-75a4-4f3b-9b70-9c837a5b3210"]
}
```
### 7.2 List, Search, Update, Delete & Bulk-Modify Contacts
- **`GET /api/v1/contacts`**:
- Query params: `page=1`, `per_page=50` (max `200`), `search=Jane`, `status_filter=active|opted_out|blocked`, `list_id=<uuid>`, `tag=vip`.
- **`GET /api/v1/contacts/{contact_id}`**: Returns contact profile, list memberships (`lists`), and recent message history (`recent_messages`).
- **`PUT /api/v1/contacts/{contact_id}`**: Update `first_name`, `last_name`, `email`, `custom_fields`, or `status`.
- **`DELETE /api/v1/contacts/{contact_id}`**: Soft-deletes the contact and removes them from all lists.
- **`POST /api/v1/contacts/bulk`**: Execute bulk actions across up to 5,000 contacts at once:
```json
{
"contact_ids": ["<uuid_1>", "<uuid_2>"],
"action": "tag",
"value": "q4-promo"
}
```
*(Supported `action` values: `"delete"`, `"tag"`, `"untag"`, `"add_to_list"`, `"opt_out"`. For `"add_to_list"`, pass the target list UUID in `"value"`.)*
### 7.3 Parse & Import Spreadsheets / CSV / Excel / JSON
- **`POST /api/v1/contacts/parse-file`** (`multipart/form-data` with `file`):
Inspects `.csv`, `.tsv`, `.txt`, `.xlsx`, `.json`, or `.jsonl` up to 25 MB and returns detected `headers`, auto-detected `effective_mapping`, `merge_fields`, and `sample_rows`.
- **`POST /api/v1/contacts/import`** (`multipart/form-data`):
- Form fields:
- `file`: The `.csv`, `.xlsx`, `.tsv`, `.json`, or `.jsonl` file
- `list_id`: Optional target `ContactList` UUID
- `mapping`: Optional JSON string mapping file columns to `phone_number`, `first_name`, `last_name`, `email`, or custom field keys
- `update_existing`: `"true"` (default) to enrich existing contacts' `custom_fields`
- **`GET /api/v1/contacts/merge-fields`**: Lists all standard and custom merge tags discovered across your tenant's contacts (`{{first_name}}`, `{{city}}`, `{{roof_type}}`, etc.).
- **`GET /api/v1/contacts/export?status_filter=active&list_id=<uuid>`**: Streams a CSV export of contacts.
### 7.4 Static & Dynamic Smart Lists (`/api/v1/lists`)
- **`POST /api/v1/lists`** (`201 Created`):
- **Static List**:
```json
{
"name": "Wyoming Homeowners",
"description": "Imported Q4 solar leads",
"is_smart": false
}
```
- **Dynamic Smart List** (automatically evaluates filter rules against your contacts):
```json
{
"name": "High-Bill VIP Leads",
"description": "Active VIP leads in Cheyenne",
"is_smart": true,
"filter_criteria": {
"status": "active",
"tag": "vip",
"custom_fields": { "city": "Cheyenne" }
}
}
```
- **`POST /api/v1/lists/smart-preview`**: Preview matching contact count and sample rows for `filter_criteria` before saving.
- **`GET /api/v1/lists`**, **`GET /api/v1/lists/{list_id}`**, **`PUT /api/v1/lists/{list_id}`**, **`DELETE /api/v1/lists/{list_id}`**.
- **`GET /api/v1/lists/{list_id}/contacts?page=1&per_page=50&search=...`**: Paginated contacts in the list.
- **`POST /api/v1/lists/{list_id}/contacts`**: Add contacts (`{"contact_ids": ["<uuid_1>", "<uuid_2>"]}`).
- **`DELETE /api/v1/lists/{list_id}/contacts`**: Remove contacts (`{"contact_ids": ["<uuid_1>"]}`).
---
## 8. Reusable Message Templates, Merge Tags, Filters & Spintax
### 8.1 Merge Tag, Filter & Spintax Syntax
Every message body in BlastWave (`/templates`, `/messages/send`, `/campaigns`, `/automations`) supports:
- **Standard Merge Variables**: `{{first_name}}`, `{{last_name}}`, `{{full_name}}`, `{{phone}}`, `{{email}}`
- **Custom Contact / CSV Fields**: Any key in `contact.custom_fields` or `row_merge_map`, e.g., `{{city}}`, `{{appointment_date}}`, `{{invoice_amount}}`
- **Default Fallbacks**: `{{first_name|there}}` -> renders `"Jane"` if `first_name` is present, or `"there"` if blank
- **Formatting Filters**: `{{first_name|title}}`, `{{code|upper}}`, `{{email|lower}}`, `{{city|capitalize}}` (can be chained with fallbacks: `{{first_name|friend|title}}`)
- **Built-In Dynamic Tokens**: `{{today}}` (e.g. `Nov 01, 2026`), `{{day_of_week}}` (e.g. `Sunday`), `{{current_year}}` (`2026`)
- **Deterministic Spintax**: `{Hi|Hello|Hey} {{first_name|there}}, {quick question|checking in}!` — selects a natural variation per recipient.
### 8.2 Template Endpoints
- **`POST /api/v1/templates`** (`201 Created`):
```json
{
"name": "Solar Inspection Reminder",
"category": "appointment",
"body": "Acme Solar: {Hi|Hello} {{first_name|there}}, your inspection in {{city|your area}} is set for {{appointment_date}}. Reply HELP for help or STOP to opt out.",
"media_urls": [],
"is_shared": true
}
```
- **`GET /api/v1/templates?page=1&per_page=50&category=appointment&search=Solar`**
- **`GET /api/v1/templates/{template_id}`**, **`PUT /api/v1/templates/{template_id}`**, **`DELETE /api/v1/templates/{template_id}`**
- **`POST /api/v1/templates/{template_id}/preview`**: Render a template against a real `contact_id` or custom `merge_data`:
```json
{
"contact_id": "4e1c2b3a-5678-4901-8234-abcdef012345",
"merge_data": { "appointment_date": "Tuesday at 2:00 PM" }
}
```
- **`POST /api/v1/templates/inspect`**: Audit any draft template for GSM-7 vs Unicode encoding, segment count, smart-quote sanitization (`sanitize_gsm7: true`), opt-out compliance, spintax combinations, and multi-row preview:
```json
{
"template": "Hi {{first_name|Friend}}, your quote of {{quote_amount}} is ready! Reply STOP to opt out.",
"sample_rows": [{"first_name": "Jane", "quote_amount": "$14,500"}],
"is_mms": false,
"sanitize_gsm7": true
}
```
---
## 9. Sending Transactional & Bulk SMS/MMS, Media Uploads & Short Links
### 9.1 Upload an MMS Image (Optional)
`POST /api/v1/media/upload` (`201 Created` — `multipart/form-data` with `file`)
Accepts JPEG, PNG, GIF, or WEBP up to **1.5 MB** (`1,572,864` bytes) and returns a carrier-reachable HTTPS URL:
```json
{
"url": "https://sms.reliadata.io/api/v1/media/files/9a1b2c3d-4567-89ab-cdef-0123456789ab/8f12c9a0b4e341d29a8f12c9a0b4e341.jpg",
"filename": "8f12c9a0b4e341d29a8f12c9a0b4e341.jpg",
"content_type": "image/jpeg",
"size_bytes": 184320,
"storage": "local"
}
```
- **Serving & Proxying MMS Media**:
- `GET /api/v1/media/files/{tenant_id}/{filename}`: Public carrier-reachable image endpoint for outbound MMS attachments.
- `GET /api/v1/media/proxy/{message_id}/{media_index}` (and authenticated `GET /api/v1/media/bandwidth-proxy/{message_id}/{media_index}`): Proxies inbound MMS attachments from Bandwidth's Basic-Auth-protected V2 Media API using 24-hour HMAC-signed URLs.
### 9.2 Create a Tracked Short Link (Optional)
`POST /api/v1/ai/shorten-link`
Creates a click-tracked short URL (`https://sms.reliadata.io/l/{code}`):
```json
{
"url": "https://acmesolar.com/schedule-consultation?utm_source=sms",
"campaign_id": null
}
```
**Response (`200 OK`):**
```json
{
"id": "e19b3c4d-1234-4a5b-8c9d-0123456789ab",
"code": "k9Xp2Q",
"short_url": "https://sms.reliadata.io/l/k9Xp2Q",
"target_url": "https://acmesolar.com/schedule-consultation?utm_source=sms"
}
```
### 9.3 Send a Single 1:1 SMS or MMS
`POST /api/v1/messages/send` (`201 Created`)
> **Flexible Sender Selection**:
> - Pass `"from_number": "+13075550140"` (or `"from": "+13075550140"`) to specify the sender DID by E.164 string.
> - Or pass `"from_number_id": "<uuid>"` to specify the sender DID by UUID.
> - Or omit both to automatically send from your tenant's primary active phone number.
```json
{
"to": "+13075550199",
"from_number": "+13075550140",
"body": "Acme Solar: Hi Jane, your solar proposal is ready: https://sms.reliadata.io/l/k9Xp2Q. Reply STOP to opt out.",
"media_urls": []
}
```
**Response (`201 Created`):**
```json
{
"id": "c72a18f4-9b12-4d3e-8f10-1a2b3c4d5e6f",
"message_id": "c72a18f4-9b12-4d3e-8f10-1a2b3c4d5e6f",
"conversation_id": "19b8a7c6-5d4e-3f2a-1b0c-9d8e7f6a5b4c",
"contact_id": "4e1c2b3a-5678-4901-8234-abcdef012345",
"bandwidth_message_id": "1698872109123abc456def",
"from_number": "+13075550140",
"to_number": "+13075550199",
"direction": "outbound",
"body": "Acme Solar: Hi Jane, your solar proposal is ready: https://sms.reliadata.io/l/k9Xp2Q. Reply STOP to opt out.",
"media_urls": [],
"status": "sent",
"segments": 1,
"cost": 0.008,
"created_at": "2026-11-01T22:20:00.123456+00:00"
}
```
### 9.4 Send a Batch of Messages (`send-bulk`)
`POST /api/v1/messages/send-bulk`
Sends up to **500** messages in a single request:
```json
{
"recipients": ["+13075550199", "+13075550188"],
"from_number": "+13075550140",
"body": "Acme Solar: Reminder that our office closes at 4 PM today for the holiday. Reply STOP to opt out.",
"media_urls": []
}
```
### 9.5 Query Message History & Real-Time Delivery Status
- **`GET /api/v1/messages`**:
- Query params: `page=1`, `per_page=50`, `direction=inbound|outbound`, `status=sent|delivered|failed`, `from_number=+13075550140`, `to_number=+13075550199`, `contact_id=<uuid>`, `conversation_id=<uuid>`, `search=proposal`.
- **`GET /api/v1/messages/{message_id}`** and **`GET /api/v1/messages/{message_id}/status`**:
- Accepts **either** the BlastWave UUID (`c72a18f4-...`) **or** the upstream `bandwidth_message_id` (`1698872109123abc456def`).
- Searches both 1:1 `messages` and `campaign_messages` and returns `status` (`queued`, `sending`, `sent`, `delivered`, `failed`), `error_code`, `error_description`, `segments`, `cost`, `sent_at`, and `delivered_at`.
---
## 10. Broadcast, A/B Test, Number-Pool & Personalized CSV-Merge Campaigns
BlastWave campaigns support standard list broadcasts, multi-DID number pools, A/B variant split-testing, per-row spreadsheet merge fields, tracked short links, and UniRoma+ AI per-recipient message customization.
### 10.1 Optional Step 1 — Upload a Campaign Merge File (`.csv`, `.xlsx`, `.json`)
`POST /api/v1/campaigns/upload-merge-file` (`201 Created` — `multipart/form-data`)
Uploads a spreadsheet or JSON file, automatically upserts the contacts into a `ContactList`, and returns `list_id`, `merge_fields`, and `row_merge_map` so every recipient's SMS can merge arbitrary columns from that file:
- Form fields: `file` (required), `list_name` (optional), `list_id` (optional), `mapping` (optional JSON string), `update_existing` (`true`).
### 10.2 Step 2 — Create a Campaign
`POST /api/v1/campaigns` (`201 Created`)
```json
{
"name": "Q4 Cheyenne Solar Rebate Blast",
"campaign_type": "blast",
"from_number_id": "a63e70e4-0a5b-4a8e-9b29-52028632056a",
"number_pool_ids": [],
"target_list_ids": ["3d6f0a8e-75a4-4f3b-9b70-9c837a5b3210"],
"exclude_list_ids": [],
"message_template": "Acme Solar: {Hi|Hello} {{first_name|there}}, homeowners in {{city|your area}} qualify for a $2,500 solar rebate! Estimate your savings: https://acmesolar.com/rebate. Reply STOP to opt out.",
"media_urls": [],
"scheduled_at": null,
"send_window_start": "09:00",
"send_window_end": "19:00",
"send_window_timezone": "America/Denver",
"throttle_mps": 10,
"ab_variants": [
{
"name": "Variant A - Rebate Dollar Amount",
"weight": 50,
"template": "Acme Solar: Hi {{first_name|there}}, claim your $2,500 {{city|local}} solar rebate before Dec 31: https://acmesolar.com/rebate. Reply STOP to opt out."
},
{
"name": "Variant B - Utility Bill Angle",
"weight": 50,
"template": "Acme Solar: Hi {{first_name|there}}, cut your {{utility_bill|monthly}} electric bill in {{city|your area}} with $0 down solar: https://acmesolar.com/rebate. Reply STOP to opt out."
}
],
"ai_customize_enabled": false,
"ai_customize_instructions": "Keep each message under 160 GSM-7 characters and preserve the opt-out footer.",
"track_links_enabled": true
}
```
*(When `"track_links_enabled": true`, every `https://...` URL in the message is automatically rewritten to a unique per-recipient `https://sms.reliadata.io/l/{code}` short link so click-through rates are tracked automatically!)*
### 10.3 Step 3 — Launch, Pause, Resume, Cancel, Clone & Retry Failed
- **`POST /api/v1/campaigns/{campaign_id}/launch`**: Resolves all active, non-opted-out recipients across `target_list_ids` (minus `exclude_list_ids`), renders merge tags/A-B variants, verifies credits, and dispatches via the Celery rate-limited sender queue. When all recipients finish, fires the `campaign.completed` webhook.
- **`POST /api/v1/campaigns/{campaign_id}/pause`**: Pauses an in-progress campaign.
- **`POST /api/v1/campaigns/{campaign_id}/resume`**: Resumes a paused campaign.
- **`POST /api/v1/campaigns/{campaign_id}/cancel`**: Cancels queued messages on a running/scheduled campaign.
- **`POST /api/v1/campaigns/{campaign_id}/clone`** (`201 Created`): Duplicates any existing campaign into a fresh `"draft"` copy.
- **`POST /api/v1/campaigns/{campaign_id}/retry-failed`**: Resets failed messages (excluding opted-out contacts) to `"queued"` and re-dispatches them.
- **`GET /api/v1/campaigns/{campaign_id}`** & **`GET /api/v1/campaigns/{campaign_id}/stats`**: Returns `total_recipients`, `sent`, `delivered`, `failed`, `opted_out`, `clicked`, `ctr`, `delivery_rate`, `cost`, and per-variant stats.
- **`GET /api/v1/campaigns/{campaign_id}/messages?page=1&per_page=50&status=delivered|failed`**: Paginated per-recipient message log.
- **`GET /api/v1/campaigns/{campaign_id}/export`**: Streams a full CSV report of all recipient messages in the campaign.
---
## 11. Two-Way Inbox, Conversations, Operator Assignment & Real-Time WebSockets
### 11.1 Inbox REST Endpoints (`/api/v1/inbox`)
- **`GET /api/v1/inbox`**:
- Query params: `page=1`, `per_page=50`, `search=Jane`, `status=open|closed|snoozed`, `assigned_to=<user_uuid>`, `unread_only=true`, `unassigned_only=true`, `mine_only=true`.
- **`GET /api/v1/inbox/{conversation_id}?message_offset=0&message_limit=50`**:
- Returns `conversation` metadata, full `contact` profile (including `lists` and `active_drips`), and chronological `messages` (with any Bandwidth inbound MMS URLs automatically rewritten to signed 24-hour proxy URLs `/api/v1/media/proxy/{message_id}/{index}`).
- **`GET /api/v1/inbox/{conversation_id}/messages?page=1&per_page=50`**: Paginated message thread (automatically marks `unread_count = 0`).
- **`POST /api/v1/inbox/{conversation_id}/reply`** (`201 Created`):
```json
{
"body": "Hi Jane! Happy to help—would Tuesday at 2 PM work for your roof inspection?",
"media_urls": []
}
```
- **`POST /api/v1/inbox/{conversation_id}/note`** (`201 Created`): Add an internal team note (`direction="internal"`, never sent to the handset):
```json
{ "body": "Customer prefers afternoon appointments. Spoke with spouse on phone." }
```
- **`POST /api/v1/inbox/{conversation_id}/assign`**: Assign or unassign a team member (`{"user_id": "<user_uuid_or_null>"}`).
- **`PUT /api/v1/inbox/{conversation_id}`**: Update `status` (`"open"`, `"closed"`, `"snoozed"`), `assigned_to`, or `tags` (`["hot-lead", "follow-up"]`).
- **`POST /api/v1/inbox/{conversation_id}/close`**: Close a conversation.
### 11.2 Real-Time WebSocket Stream (`wss://sms.reliadata.io/ws`)
Connect with a valid JWT access token to receive live inbox messages and delivery status updates in real time:
```
wss://sms.reliadata.io/ws?token=<jwt_access_token>
```
- Send `"ping"` text frames every 30s -> receives `{"type": "pong"}`.
- Pushes real-time JSON events (`inbound_message`, `message_status`, `conversation_updated`) scoped strictly to your `tenant_id`.
---
## 12. Automations: Keyword Auto-Replies & Multi-Step Drip Sequences
### 12.1 Keyword, Event & After-Hours Auto-Replies (`/api/v1/automations/auto-replies`)
Auto-replies evaluate inbound SMS messages in order of `priority` (higher integer runs first).
> **Valid `trigger_type` Values**:
> - `"keyword"`: Exact case-insensitive match on one or more comma-separated keywords in `trigger_value` (e.g. `"PRICING,QUOTE,COST"`).
> - `"contains"`: Inbound message body contains any comma-separated phrase in `trigger_value`.
> - `"first_message"`: Triggers on the first inbound message ever received from a contact (`trigger_value` can be `null`).
> - `"after_hours"`: Triggers when an inbound message arrives outside business hours (`trigger_value` can be `null`).
> - `"opt_in"`: Triggers when a contact texts an opt-in keyword.
> - `"all"`: Catch-all responder (`trigger_value` can be `null`).
- **`POST /api/v1/automations/auto-replies`** (`201 Created`):
```json
{
"phone_number_id": "a63e70e4-0a5b-4a8e-9b29-52028632056a",
"trigger_type": "keyword",
"trigger_value": "PRICING,QUOTE,ESTIMATE",
"response_body": "Acme Solar: Hi {{first_name|there}}! Residential solar starts at $0 down / $99/mo. Get an instant roof quote at https://acmesolar.com/quote",
"media_urls": [],
"is_active": true,
"priority": 10
}
```
*(Pass `"phone_number_id": null` to apply the rule across all phone numbers in your tenant.)*
- **`GET /api/v1/automations/auto-replies?page=1&per_page=50&is_active=true`**
- **`PUT /api/v1/automations/auto-replies/{auto_reply_id}`**
- **`DELETE /api/v1/automations/auto-replies/{auto_reply_id}`**
### 12.2 Multi-Step Drip Sequences (`/api/v1/automations/drip-sequences`)
Drip sequences send timed, multi-step follow-up messages and automatically pause/complete if a contact replies (when `"condition": {"no_reply": true}`) or opts out.
> **Valid `trigger_event` Values**: `"contact_created"`, `"tag_added"`, `"manual"`, `"keyword_opt_in"`
1. **Create a Drip Sequence (`POST /api/v1/automations/drip-sequences`, `201 Created`)**:
```json
{
"name": "New Solar Lead 3-Day Nurture",
"trigger_event": "manual",
"steps": [
{
"step_order": 1,
"delay_minutes": 5,
"message_template": "Acme Solar: Hi {{first_name|there}}, thanks for requesting a solar assessment! Do you know your approximate monthly electric bill? Reply STOP to opt out.",
"media_urls": [],
"condition": null
},
{
"step_order": 2,
"delay_minutes": 1440,
"message_template": "Acme Solar: Hi {{first_name|there}}, just following up—we have 2 inspection slots open in {{city|your area}} this week. Reply YES to claim one!",
"media_urls": [],
"condition": { "no_reply": true }
},
{
"step_order": 3,
"delay_minutes": 4320,
"message_template": "Acme Solar: Last check-in {{first_name|there}}! You can view our self-service savings calculator anytime at https://acmesolar.com/calc. Reply STOP to opt out.",
"media_urls": [],
"condition": { "no_reply": true }
}
]
}
```
2. **Activate the Drip Sequence (`POST /api/v1/automations/drip-sequences/{sequence_id}/activate`)**:
```json
{ "active": true }
```
3. **Enroll Contacts or Entire Lists (`POST /api/v1/automations/drip-sequences/{sequence_id}/enroll`, `201 Created`)**:
```json
{
"contact_ids": ["4e1c2b3a-5678-4901-8234-abcdef012345"],
"list_ids": ["3d6f0a8e-75a4-4f3b-9b70-9c837a5b3210"]
}
```
Returns `{"enrolled": [...], "skipped": [...], "enrolled_count": 42, "skipped_count": 1}`.
4. **Inspect Sequence & Enrollments**:
- `GET /api/v1/automations/drip-sequences`
- `GET /api/v1/automations/drip-sequences/{sequence_id}` (includes `steps` and `enrollment_stats`: `active`, `completed`, `paused`, `canceled`)
- `GET /api/v1/automations/drip-sequences/{sequence_id}/enrollments?page=1&per_page=50&status=active`
- `PUT /api/v1/automations/drip-sequences/{sequence_id}`
- `DELETE /api/v1/automations/drip-sequences/{sequence_id}`
---
## 13. Conversational AI Agents & UniRoma+ AI Copywriting Suite
BlastWave includes **autonomous 24/7 Conversational SMS AI Agents** powered by **UniRoma+** (plus OpenAI/Anthropic fallback support) that answer inbound customer texts using your custom knowledge base, maintain multi-turn conversation history, and automatically escalate to a human operator when needed.
### 13.1 How Conversational AI Agents Work on Inbound Texts
When an inbound SMS arrives on a tenant phone number:
1. **TCPA Opt-Out / Help First**: Standard `STOP` / `HELP` keywords are processed first by the compliance engine.
2. **Active AI Agent Routing**: If an active `AIAgent` (`is_active: true`) has that `phone_number_id` in its `phone_number_ids` list:
- BlastWave checks `escalation_rules`:
- If the inbound message matches any escalation keyword (e.g. `"human"`, `"manager"`, `"complaint"`), or the conversation has reached `max_turns`, or negative sentiment triggers escalation, the agent sends the configured `escalation_message`, marks the conversation for human follow-up, logs `escalated: true`, and fires the **`ai.escalation`** outbound customer webhook.
- Otherwise, the AI agent builds a prompt combining your `system_prompt`, structured `knowledge_base`, contact profile (`first_name`, `custom_fields`), and the recent conversation transcript, generates a concise SMS reply via `UniRoma+`, transmits it over Bandwidth, and logs latency and token usage in `AIAgentLog`.
### 13.2 Create a Conversational AI Agent
`POST /api/v1/ai-agents` (`201 Created`)
> **Plan Quotas**: `free_trial` = 0 AI agents, `starter` = 1 AI agent, `growth` = 3 AI agents, `enterprise` = 10 AI agents.
```json
{
"name": "Acme Solar 24/7 Concierge & Booking Agent",
"phone_number_ids": ["a63e70e4-0a5b-4a8e-9b29-52028632056a"],
"system_prompt": "You are Sunny, the friendly SMS concierge for Acme Solar in Cheyenne, WY. Keep responses under 300 characters, answer questions accurately using the knowledge base, and ask leads for their street address and average monthly electric bill to schedule a free roof inspection.",
"model": "UniRoma+",
"temperature": 0.4,
"max_tokens": 250,
"knowledge_base": {
"company_name": "Acme Solar LLC",
"business_hours": "Mon-Fri 8:00 AM - 6:00 PM MST, Sat 9:00 AM - 2:00 PM MST",
"service_areas": "Cheyenne, Laramie, Casper, and Northern Colorado",
"pricing_and_financing": "$0 down financing available at 3.99% APR; typical residential systems start at $99/month.",
"warranty": "25-year bumper-to-bumper panel, inverter, and roof penetration warranty.",
"booking_link": "https://acmesolar.com/book",
"faqs": [
{
"question": "How long does installation take?",
"answer": "Most residential installations take 1 to 2 days once utility permits are approved."
},
{
"question": "Do solar panels work in Wyoming winters?",
"answer": "Yes! Cold sunny days actually improve panel voltage efficiency, and panels shed snow quickly on tilted mounts."
}
]
},
"escalation_rules": {
"keywords": ["human", "person", "operator", "manager", "supervisor", "lawyer", "cancel contract", "refund"],
"max_turns": 8,
"sentiment_threshold": "negative",
"escalation_message": "I'm connecting you with an Acme Solar specialist right now. A team member will text or call you shortly!"
}
}
```
**Response (`201 Created`):**
```json
{
"agent": {
"id": "8c4d2e1f-9a0b-4c3d-8e7f-6a5b4c3d2e1f",
"name": "Acme Solar 24/7 Concierge & Booking Agent",
"is_active": false,
"phone_number_ids": ["a63e70e4-0a5b-4a8e-9b29-52028632056a"],
"system_prompt": "You are Sunny, the friendly SMS concierge for Acme Solar...",
"model": "UniRoma+",
"temperature": 0.4,
"max_tokens": 250,
"knowledge_base": { "...": "..." },
"escalation_rules": { "...": "..." },
"created_at": "2026-11-01T22:30:00+00:00"
}
}
```
### 13.3 Test, Activate, Update & Monitor an AI Agent
1. **Test the AI Agent in Sandbox (No SMS Sent / No Credits Used)**:
`POST /api/v1/ai-agents/{agent_id}/test`
```json
{
"message": "Hi, do your panels work when it snows in Cheyenne and how much is the down payment?"
}
```
**Response (`200 OK`):**
```json
{
"test_message": "Hi, do your panels work when it snows in Cheyenne and how much is the down payment?",
"ai_response": "Hi there! Yes, our panels perform great in Cheyenne winters—cold sunny weather actually boosts efficiency, and tilted mounts shed snow quickly. Plus, we offer $0 down financing starting at $99/mo! What's your approx monthly electric bill?",
"model": "UniRoma+",
"tokens_used": 94,
"latency_ms": 410
}
```
2. **Activate or Update the AI Agent**:
`PUT /api/v1/ai-agents/{agent_id}`
```json
{
"is_active": true,
"phone_number_ids": ["a63e70e4-0a5b-4a8e-9b29-52028632056a"],
"temperature": 0.35
}
```
3. **Get AI Agent Stats & Interaction Logs**:
- **`GET /api/v1/ai-agents`**: List all AI agents in the tenant.
- **`GET /api/v1/ai-agents/{agent_id}`**: Returns `agent` configuration plus aggregate `stats` (`total_interactions`, `avg_latency_ms`, `total_tokens_used`, `escalation_count`, `escalation_rate`).
- **`GET /api/v1/ai-agents/{agent_id}/logs?page=1&per_page=50&escalated_only=false`**: Paginated interaction logs (`inbound_message`, `ai_response`, `model_used`, `tokens_used`, `latency_ms`, `escalated`, `escalation_reason`, `created_at`).
- **`DELETE /api/v1/ai-agents/{agent_id}`**: Delete an AI agent and its logs.
### 13.4 UniRoma+ AI Copywriting & Inbox Assistant (`/api/v1/ai`)
- **`GET /api/v1/ai/status`**: Check AI provider status (`{"provider": "UniRoma+", "uniroma_configured": true}`).
- **`POST /api/v1/ai/rewrite`**: Rewrite a draft SMS for a specific goal (`"friendly"`, `"urgent"`, `"concise"`, `"professional"`, `"high_converting"`):
```json
{
"draft": "Hey we have solar panels on sale in your city call us back",
"goal": "high_converting",
"available_merge_fields": ["first_name", "city"],
"include_opt_out": true,
"include_spintax": true
}
```
- **`POST /api/v1/ai/generate-variants`**: Generate 2–5 distinct A/B test variants for a campaign:
```json
{
"draft": "Acme Solar: Claim your $2,500 solar rebate in {{city}} before Dec 31.",
"count": 3,
"available_merge_fields": ["first_name", "city"],
"include_opt_out": true
}
```
- **`POST /api/v1/ai/customize-preview`**: Preview how UniRoma+ customizes a campaign template per recipient using their contact profile and spreadsheet columns:
```json
{
"template": "Acme Solar: Hi {{first_name}}, see how much you can save on your {{utility_bill}} bill in {{city}}!",
"list_ids": ["3d6f0a8e-75a4-4f3b-9b70-9c837a5b3210"],
"instructions": "Keep under 160 GSM-7 characters and reference their roof_type if present.",
"limit": 5,
"single_segment_target": true
}
```
- **`POST /api/v1/ai/inbox-suggest`**: Generate a suggested operator reply for an open inbox conversation:
```json
{
"conversation_id": "19b8a7c6-5d4e-3f2a-1b0c-9d8e7f6a5b4c",
"tone": "helpful"
}
```
---
## 14. Outbound Customer Webhooks (All 8 Events + HMAC-SHA256 Verification)
BlastWave pushes real-time HTTP `POST` webhook events to your server whenever messages are sent, delivered, failed, or received, when contacts opt out/in, when campaigns complete, or when an AI agent escalates a conversation.
### 14.1 Managing Webhook Endpoints
Both `/api/v1/webhooks/endpoints*` and `/api/v1/tenants/current/webhooks*` are supported and backed by the same store:
- **`POST /api/v1/webhooks/endpoints`** (`201 Created`):
```json
{
"url": "https://api.yourcompany.com/webhooks/blastwave",
"description": "Production CRM & ERP Event Listener",
"events": ["*"],
"is_active": true
}
```
*(Pass `["*"]` for all events, or subscribe to specific events from the 8 listed below. SSRF protection requires a public `https://` or `http://` URL.)*
- **`GET /api/v1/webhooks/endpoints`** (alias `GET /api/v1/tenants/current/webhooks`): List registered endpoints (`endpoints`) and masked `signing_secret_masked`.
- **`GET /api/v1/webhooks/endpoints/{endpoint_id}`** (alias `GET /api/v1/tenants/current/webhooks/{endpoint_id}`): Get a single webhook endpoint.
- **`PUT /api/v1/webhooks/endpoints/{endpoint_id}`** (alias `PUT /api/v1/tenants/current/webhooks/{endpoint_id}`): Update `url`, `description`, `events`, or `is_active`.
- **`DELETE /api/v1/webhooks/endpoints/{endpoint_id}`** (alias `DELETE /api/v1/tenants/current/webhooks/{endpoint_id}`): Remove an endpoint.
- **`GET /api/v1/webhooks/secret`** (alias `GET /api/v1/tenants/current/webhooks/secret`): Retrieve the full `whsec_...` HMAC-SHA256 signing secret.
- **`POST /api/v1/webhooks/rotate-secret`** (alias `POST /api/v1/tenants/current/webhooks/rotate-secret`): Generate a new `whsec_...` signing secret immediately.
- **`POST /api/v1/webhooks/test`** (alias `POST /api/v1/tenants/current/webhooks/test`): Dispatch a live signed `webhook.test` ping (`{"endpoint_id": "<optional_uuid>"}`) and inspect the HTTP response status and body.
- **`GET /api/v1/webhooks/deliveries?limit=50`** (alias `GET /api/v1/tenants/current/webhooks/deliveries`): View recent webhook delivery logs (`delivery_id`, `event`, `url`, `status_code`, `ok`, `attempt`, `response_preview`, `delivered_at`).
- *(Note: `POST /api/v1/webhooks/bandwidth` and `POST /api/v1/webhooks/stripe` are internal carrier/payment receiver endpoints for Bandwidth V2 and Stripe callbacks.)*
### 14.2 Webhook Request Headers & Retry Policy
Every outbound webhook `POST` includes:
```http
Content-Type: application/json
User-Agent: BlastWave-Webhook/1.0
X-BlastWave-Event: message.received
X-BlastWave-Delivery: 7c9e6679-7425-40de-944b-e07fc1f90ae7
X-BlastWave-Timestamp: 1762039200
X-BlastWave-Signature: t=1762039200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```
- **Timeout & Retries**: 10-second timeout per attempt. Any `2xx` status code acknowledges receipt. On `4xx`/`5xx` or network timeout, BlastWave retries up to **3 times** with exponential backoff.
### 14.3 All 8 Outbound Webhook Event Payloads
Every webhook shares this top-level envelope:
```json
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"event": "<event_name>",
"created_at": "2026-11-01T22:40:00.000000+00:00",
"tenant_id": "9a1b2c3d-4567-89ab-cdef-0123456789ab",
"data": { ... }
}
```
1. **`message.sent`** (fired on `POST /messages/send`, `/messages/send-bulk`, `/inbox/{id}/reply`, drip sequences, and auto-replies):
```json
{
"message_id": "c72a18f4-9b12-4d3e-8f10-1a2b3c4d5e6f",
"conversation_id": "19b8a7c6-5d4e-3f2a-1b0c-9d8e7f6a5b4c",
"contact_id": "4e1c2b3a-5678-4901-8234-abcdef012345",
"bandwidth_message_id": "1698872109123abc456def",
"direction": "outbound",
"from_number": "+13075550140",
"to_number": "+13075550199",
"body": "Acme Solar: Hi Jane, your solar proposal is ready...",
"media_urls": [],
"segments": 1,
"cost": 0.008,
"status": "sent"
}
```
2. **`message.delivered`** (fired when Bandwidth confirms handset delivery):
```json
{
"message_id": "c72a18f4-9b12-4d3e-8f10-1a2b3c4d5e6f",
"campaign_id": null,
"bandwidth_message_id": "1698872109123abc456def",
"from_number": "+13075550140",
"to_number": "+13075550199",
"status": "delivered",
"delivered_at": "2026-11-01T22:40:03.112000+00:00"
}
```
3. **`message.failed`** (fired on carrier rejection or undeliverable DLR):
```json
{
"message_id": "c72a18f4-9b12-4d3e-8f10-1a2b3c4d5e6f",
"campaign_id": null,
"bandwidth_message_id": "1698872109123abc456def",
"from_number": "+13075550140",
"to_number": "+13075550199",
"status": "failed",
"error_code": "4720",
"error_description": "Invalid destination number"
}
```
4. **`message.received`** (fired on every inbound SMS/MMS from a handset):
```json
{
"message_id": "d83b29e5-0c23-4e4f-9a21-2b3c4d5e6f7a",
"conversation_id": "19b8a7c6-5d4e-3f2a-1b0c-9d8e7f6a5b4c",
"contact_id": "4e1c2b3a-5678-4901-8234-abcdef012345",
"bandwidth_message_id": "1698872199000xyz987",
"direction": "inbound",
"from_number": "+13075550199",
"to_number": "+13075550140",
"body": "Yes, Tuesday at 2 PM works great!",
"media_urls": []
}
```
5. **`contact.opted_out`** (fired on inbound `STOP`/opt-out phrase, `POST /compliance/opt-outs`, or `POST /contacts/bulk` `opt_out`):
```json
{
"contact_id": "4e1c2b3a-5678-4901-8234-abcdef012345",
"phone_number": "+13075550199",
"to_number": "+13075550140",
"keyword": "STOP",
"source": "inbound_sms"
}
```
6. **`contact.opted_in`** (fired on inbound `START`/`UNSTOP` or `DELETE /compliance/opt-outs`):
```json
{
"contact_id": "4e1c2b3a-5678-4901-8234-abcdef012345",
"phone_number": "+13075550199",
"to_number": "+13075550140",
"keyword": "START",
"source": "inbound_sms"
}
```
7. **`campaign.completed`** (fired when a broadcast campaign finishes sending to all recipients):
```json
{
"campaign_id": "91a2b3c4-d5e6-4f7a-8b9c-0d1e2f3a4b5c",
"name": "Q4 Cheyenne Solar Rebate Blast",
"status": "completed",
"total_recipients": 1250,
"sent_count": 1250,
"delivered_count": 1228,
"failed_count": 22
}
```
8. **`ai.escalation`** (fired when a Conversational AI Agent escalates a conversation to a human operator):
```json
{
"agent_id": "8c4d2e1f-9a0b-4c3d-8e7f-6a5b4c3d2e1f",
"agent_name": "Acme Solar 24/7 Concierge & Booking Agent",
"conversation_id": "19b8a7c6-5d4e-3f2a-1b0c-9d8e7f6a5b4c",
"contact_id": "4e1c2b3a-5678-4901-8234-abcdef012345",
"from_number": "+13075550199",
"to_number": "+13075550140",
"inbound_message": "I need to speak with a manager about my installation permit.",
"reason": "Matched escalation keyword: manager"
}
```
### 14.4 Verifying `X-BlastWave-Signature` (Python, Node.js, PHP)
Signed string format: `f"{timestamp}.{raw_request_body_utf8}"` hashed with HMAC-SHA256 using your `whsec_...` signing secret.
#### Python (FastAPI / Flask)
```python
import hashlib
import hmac
import time
def verify_blastwave_webhook(raw_body: bytes, signature_header: str, secret: str, tolerance_sec: int = 300) -> bool:
parts = dict(item.split("=", 1) for item in signature_header.split(",") if "=" in item)
ts_str, sig_v1 = parts.get("t"), parts.get("v1")
if not ts_str or not sig_v1:
return False
if abs(time.time() - int(ts_str)) > tolerance_sec:
return False
signed_payload = f"{ts_str}.".encode("utf-8") + raw_body
expected = hmac.new(secret.encode("utf-8"), signed_payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig_v1)
```
#### Node.js (Express / Next.js)
```javascript
import crypto from "node:crypto";
export function verifyBlastWaveWebhook(rawBodyBuffer, signatureHeader, secret, toleranceSec = 300) {
const parts = Object.fromEntries(
(signatureHeader || "").split(",").map((p) => p.split("=", 2))
);
if (!parts.t || !parts.v1) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t)) > toleranceSec) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.`)
.update(rawBodyBuffer)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(parts.v1, "hex"));
}
```
#### PHP (Laravel / Symfony)
```php
<?php
function verifyBlastWaveWebhook(string $rawBody, string $signatureHeader, string $secret, int $toleranceSec = 300): bool {
$parts = [];
foreach (explode(',', $signatureHeader) as $segment) {
[$k, $v] = array_pad(explode('=', $segment, 2), 2, '');
$parts[$k] = $v;
}
if (empty($parts['t']) || empty($parts['v1'])) return false;
if (abs(time() - (int)$parts['t']) > $toleranceSec) return false;
$expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
return hash_equals($expected, $parts['v1']);
}
```
---
## 15. Analytics, Carrier Error Diagnostics & CSV Exports
- **`GET /api/v1/analytics/dashboard`**: Real-time KPIs (`messages_sent_today`, `messages_delivered_today`, `messages_failed_today`, `responses_today`, `delivery_rate`, `active_contacts`, `active_numbers`, `spend_today`).
- **`GET /api/v1/analytics/volume?period=30d`** (or `start_date=2026-10-01&end_date=2026-11-01`): Daily sent/delivered/failed time series plus `spend_breakdown` (`sms_spend`, `mms_spend`, `number_spend`, `refunds`, `total_spend`).
- **`GET /api/v1/analytics/campaigns?period=30d&sort_by=delivered`**: Campaign performance leaderboard with `delivery_rate` and `click_rate`.
- **`GET /api/v1/analytics/campaigns/{campaign_id}`**: Detailed campaign analytics and daily delivery timeline.
- **`GET /api/v1/analytics/numbers?period=7d`**: Per-DID outbound, inbound, delivered counts, and `delivery_rate`.
- **`GET /api/v1/analytics/contacts?period=30d`**: Daily new contact growth and breakdown (`active_contacts`, `opted_out_contacts`, `bounced_contacts`).
- **`GET /api/v1/analytics/carrier-errors?period=30d`**: Aggregates carrier DLR error codes (`4720`, `4770` spam filter, `4775` daily cap, etc.) with human-readable remediation guidance and 10DLC daily-cap circuit-breaker state.
- **`GET /api/v1/analytics/export?report_type=messages|campaigns|contacts&period=30d`**: Streams CSV report download.
---
## 16. Complete End-to-End SDK Clients (Python & TypeScript)
### 16.1 Python Full-Lifecycle SDK (`BlastWaveClient`)
Covers searching & buying phone numbers, 10DLC compliance, contacts & lists, templates, 1:1 & bulk SMS/MMS, campaigns, inbox replies, auto-replies, drip sequences, AI agents, UniRoma+ AI, and webhooks:
```python
from __future__ import annotations
from typing import Any
import httpx
class BlastWaveClient:
"""Full-featured synchronous/async-ready Python SDK for BlastWave SMS API v1."""
def __init__(
self,
api_key: str,
base_url: str = "https://sms.reliadata.io/api/v1",
timeout: float = 30.0,
) -> None:
self.base_url = base_url.rstrip("/")
self._client = httpx.Client(
base_url=self.base_url,
headers={
"X-API-Key": api_key,
"Content-Type": "application/json",
"Accept": "application/json",
},
timeout=timeout,
)
def _req(self, method: str, path: str, **kwargs: Any) -> Any:
resp = self._client.request(method, path, **kwargs)
resp.raise_for_status()
return resp.json()
# --- 1. Phone Numbers ---
def search_numbers(
self,
area_code: str | None = None,
state: str | None = None,
number_type: str = "local",
quantity: int = 10,
) -> list[dict[str, Any]]:
return self._req(
"POST",
"/numbers/search",
json={
"area_code": area_code,
"state": state,
"number_type": number_type,
"quantity": quantity,
},
)["available_numbers"]
def order_numbers(
self, numbers: list[str], campaign_id: str | None = None
) -> dict[str, Any]:
return self._req(
"POST",
"/numbers/order",
json={"numbers": numbers, "campaign_id": campaign_id},
)
def list_numbers(self) -> list[dict[str, Any]]:
return self._req("GET", "/numbers")["numbers"]
def update_number(
self,
number_id: str,
friendly_name: str | None = None,
campaign_id: str | None = None,
unassign_campaign: bool = False,
) -> dict[str, Any]:
return self._req(
"PUT",
f"/numbers/{number_id}",
json={
"friendly_name": friendly_name,
"campaign_id": campaign_id,
"unassign_campaign": unassign_campaign,
},
)
# --- 2. 10DLC & Toll-Free Compliance ---
def register_10dlc_brand(self, brand_payload: dict[str, Any]) -> dict[str, Any]:
return self._req("POST", "/compliance/brands/apply", json=brand_payload)
def generate_10dlc_campaign_copy(
self, brand_id: str, use_case: str = "MIXED", business_goal: str | None = None
) -> dict[str, Any]:
return self._req(
"POST",
"/compliance/campaigns/ai-generate",
json={
"brand_id": brand_id,
"use_case": use_case,
"business_goal": business_goal,
"auto_ensure_minisite": True,
},
)
def register_10dlc_campaign(self, campaign_payload: dict[str, Any]) -> dict[str, Any]:
return self._req("POST", "/compliance/campaigns/apply", json=campaign_payload)
def submit_toll_free_verification(self, tfv_payload: dict[str, Any]) -> dict[str, Any]:
return self._req("POST", "/compliance/toll-free/verify", json=tfv_payload)
# --- 3. Contacts & Lists ---
def create_contact(
self,
phone_number: str,
first_name: str | None = None,
last_name: str | None = None,
email: str | None = None,
custom_fields: dict[str, Any] | None = None,
list_ids: list[str] | None = None,
) -> dict[str, Any]:
return self._req(
"POST",
"/contacts",
json={
"phone_number": phone_number,
"first_name": first_name,
"last_name": last_name,
"email": email,
"custom_fields": custom_fields or {},
"list_ids": list_ids or [],
},
)["contact"]
def create_list(
self,
name: str,
description: str = "",
is_smart: bool = False,
filter_criteria: dict[str, Any] | None = None,
) -> dict[str, Any]:
return self._req(
"POST",
"/lists",
json={
"name": name,
"description": description,
"is_smart": is_smart,
"filter_criteria": filter_criteria,
},
)["list"]
# --- 4. Sending SMS / MMS & Short Links ---
def shorten_link(self, url: str, campaign_id: str | None = None) -> dict[str, Any]:
return self._req(
"POST", "/ai/shorten-link", json={"url": url, "campaign_id": campaign_id}
)
def send_message(
self,
to: str,
body: str,
from_number: str | None = None,
media_urls: list[str] | None = None,
) -> dict[str, Any]:
payload: dict[str, Any] = {
"to": to,
"body": body,
"media_urls": media_urls or [],
}
if from_number:
payload["from_number"] = from_number
return self._req("POST", "/messages/send", json=payload)
def send_bulk(
self,
recipients: list[str],
body: str,
from_number: str | None = None,
media_urls: list[str] | None = None,
) -> dict[str, Any]:
return self._req(
"POST",
"/messages/send-bulk",
json={
"recipients": recipients,
"from_number": from_number,
"body": body,
"media_urls": media_urls or [],
},
)
def get_message_status(self, message_id_or_bw_id: str) -> dict[str, Any]:
return self._req("GET", f"/messages/{message_id_or_bw_id}/status")
# --- 5. Campaigns ---
def create_and_launch_campaign(
self,
name: str,
from_number_id: str,
target_list_ids: list[str],
message_template: str,
track_links_enabled: bool = True,
) -> dict[str, Any]:
camp = self._req(
"POST",
"/campaigns",
json={
"name": name,
"from_number_id": from_number_id,
"target_list_ids": target_list_ids,
"message_template": message_template,
"track_links_enabled": track_links_enabled,
},
)["campaign"]
return self._req("POST", f"/campaigns/{camp['id']}/launch")
# --- 6. Automations (Auto-Replies & Drip Sequences) ---
def create_auto_reply(
self,
trigger_type: str,
trigger_value: str | None,
response_body: str,
phone_number_id: str | None = None,
priority: int = 10,
) -> dict[str, Any]:
return self._req(
"POST",
"/automations/auto-replies",
json={
"phone_number_id": phone_number_id,
"trigger_type": trigger_type,
"trigger_value": trigger_value,
"response_body": response_body,
"is_active": True,
"priority": priority,
},
)["auto_reply"]
def create_and_activate_drip(
self, name: str, trigger_event: str, steps: list[dict[str, Any]]
) -> dict[str, Any]:
seq = self._req(
"POST",
"/automations/drip-sequences",
json={"name": name, "trigger_event": trigger_event, "steps": steps},
)["drip_sequence"]
return self._req(
"POST",
f"/automations/drip-sequences/{seq['id']}/activate",
json={"active": True},
)
def enroll_in_drip(
self,
sequence_id: str,
contact_ids: list[str] | None = None,
list_ids: list[str] | None = None,
) -> dict[str, Any]:
return self._req(
"POST",
f"/automations/drip-sequences/{sequence_id}/enroll",
json={"contact_ids": contact_ids or [], "list_ids": list_ids or []},
)
# --- 7. Conversational AI Agents ---
def create_and_activate_ai_agent(
self,
name: str,
phone_number_ids: list[str],
system_prompt: str,
knowledge_base: dict[str, Any],
escalation_rules: dict[str, Any],
model: str = "UniRoma+",
temperature: float = 0.4,
) -> dict[str, Any]:
agent = self._req(
"POST",
"/ai-agents",
json={
"name": name,
"phone_number_ids": phone_number_ids,
"system_prompt": system_prompt,
"model": model,
"temperature": temperature,
"max_tokens": 250,
"knowledge_base": knowledge_base,
"escalation_rules": escalation_rules,
},
)["agent"]
return self._req(
"PUT", f"/ai-agents/{agent['id']}", json={"is_active": True}
)["agent"]
def test_ai_agent(self, agent_id: str, message: str) -> dict[str, Any]:
return self._req(
"POST", f"/ai-agents/{agent_id}/test", json={"message": message}
)
# --- 8. Outbound Webhooks ---
def register_webhook(
self, url: str, events: list[str] | None = None, description: str = ""
) -> dict[str, Any]:
return self._req(
"POST",
"/webhooks/endpoints",
json={
"url": url,
"events": events or ["*"],
"description": description,
"is_active": True,
},
)
```
### 16.2 TypeScript / Node.js Full-Lifecycle SDK (`BlastWaveSDK`)
```typescript
export class BlastWaveSDK {
constructor(
private readonly apiKey: string,
private readonly baseUrl: string = "https://sms.reliadata.io/api/v1"
) {}
private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
const res = await fetch(`${this.baseUrl}${path}`, {
method,
headers: {
"X-API-Key": this.apiKey,
"Content-Type": "application/json",
Accept: "application/json",
},
body: body !== undefined ? JSON.stringify(body) : undefined,
});
if (!res.ok) {
throw new Error(`BlastWave API ${res.status}: ${await res.text()}`);
}
return (await res.json()) as T;
}
// 1. Search & Order Phone Numbers
searchNumbers(params: { area_code?: string; state?: string; number_type?: "local" | "toll_free"; quantity?: number }) {
return this.request<{ available_numbers: Array<{ number: string; city: string; state: string; monthly_cost: number }> }>(
"POST",
"/numbers/search",
params
);
}
orderNumbers(numbers: string[], campaignId?: string) {
return this.request<{ ordered: number; numbers: Array<{ id: string; number: string }> }>(
"POST",
"/numbers/order",
{ numbers, campaign_id: campaignId ?? null }
);
}
// 2. Contacts & Lists
upsertContact(payload: {
phone_number: string;
first_name?: string;
last_name?: string;
email?: string;
custom_fields?: Record<string, unknown>;
list_ids?: string[];
}) {
return this.request<{ contact: { id: string; phone_number: string; status: string } }>(
"POST",
"/contacts",
payload
);
}
// 3. Send SMS / MMS
sendMessage(payload: { to: string; body: string; from_number?: string; media_urls?: string[] }) {
return this.request<{
id: string;
bandwidth_message_id: string;
status: string;
segments: number;
cost: number;
}>("POST", "/messages/send", payload);
}
// 4. Conversational AI Agents
createAiAgent(payload: {
name: string;
phone_number_ids: string[];
system_prompt: string;
knowledge_base: Record<string, unknown>;
escalation_rules: Record<string, unknown>;
model?: string;
temperature?: number;
}) {
return this.request<{ agent: { id: string; name: string; is_active: boolean } }>(
"POST",
"/ai-agents",
payload
);
}
activateAiAgent(agentId: string, isActive = true) {
return this.request<{ agent: { id: string; is_active: boolean } }>(
"PUT",
`/ai-agents/${agentId}`,
{ is_active: isActive }
);
}
testAiAgent(agentId: string, message: string) {
return this.request<{ ai_response: string; latency_ms: number; tokens_used: number }>(
"POST",
`/ai-agents/${agentId}/test`,
{ message }
);
}
// 5. Outbound Webhooks
registerWebhook(url: string, events: string[] = ["*"], description = "") {
return this.request<{ endpoint: { id: string; url: string; events: string[] }; signing_secret: string }>(
"POST",
"/webhooks/endpoints",
{ url, events, description, is_active: true }
);
}
}
```