> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Send & Receive Messages

> Send your first message and receive inbound replies via the Orbit API — one end-to-end walkthrough covering the send call, delivery tracking, and inbound webhooks.

# Send & Receive Messages

This guide takes you from an API key to a working two-way message flow: send an outbound message, track its delivery, and receive the reply on your own server. SMS is used as the running example — the same three-step pattern (send → track → receive) applies to WhatsApp, RCS, Viber, and Email, each on its own endpoint.

**You will:**

1. [Send a message](#1-send-a-message)
2. [Track delivery](#2-track-delivery)
3. [Receive inbound messages](#3-receive-inbound-messages)
4. [Reply to an inbound message](#4-reply-to-an-inbound-message)

## Prerequisites

* An API key from **Settings → API Keys**. Use a sandbox key (`dv_test_sk_…`) while you build — sandbox sends are free and simulated, and return deterministic delivery receipts so you can exercise both the success and failure paths. Swap in a live key (`dv_live_sk_…`) for production.
* A sender number that can send on the channel you're using. For SMS that's an SMS-capable number you own (search and buy one via the [Numbers API](/api-reference/numbers), or from **Dashboard → Numbers**). Omit `from` and Orbit picks an eligible sender for the destination.
* A public HTTPS URL for the [receive](#3-receive-inbound-messages) step. Any tunneling tool works while developing.

All requests go to a single base URL — sandbox is the same host, selected by your key, not a different domain:

```
https://api.orbit.devotel.io/api/v1
```

Every request carries your key in the `X-API-Key` header.

## 1. Send a message

Send an SMS with `POST /messages/sms`. Only `to` and `body` are required; `from` is optional.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/sms \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-conf-98421" \
  -d '{
    "to": "+14155552671",
    "from": "+18005551234",
    "body": "Your order #1234 has shipped. Reply STATUS for tracking."
  }'
```

The response is `202 Accepted` — the message is persisted and queued for delivery, not yet handed to the carrier. Fields live under `data`; `meta` carries the `request_id` you should log for support.

```json theme={null}
{
  "data": {
    "id": "msg_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
    "status": "queued",
    "channel": "sms",
    "direction": "outbound",
    "segments": 1
  },
  "meta": {
    "request_id": "req_xyz789",
    "timestamp": "2026-07-20T00:00:00Z"
  }
}
```

The `id` (`msg_` followed by 32 hex characters) is the handle for every follow-up call — status lookups, delivery webhooks, and the message trace all key off it. Sandbox responses add `"test_mode": true` to `meta`.

<Note>
  Always send an `Idempotency-Key` on sends. Replaying the same key and body within 24 hours returns the original response instead of sending a duplicate; replaying it with a different body returns `409 IDEMPOTENCY_KEY_REUSED`.
</Note>

### Other channels

Each channel has its own endpoint under the `/messages` prefix with a body shape suited to that channel. There is no single channel-polymorphic route — pick the endpoint that matches your channel:

| Channel  | Endpoint                  | Reference                                           |
| -------- | ------------------------- | --------------------------------------------------- |
| SMS      | `POST /messages/sms`      | [Messaging API](/api-reference/endpoints/messaging) |
| WhatsApp | `POST /messages/whatsapp` | [WhatsApp channel](/channels/whatsapp)              |
| RCS      | `POST /messages/rcs`      | [RCS channel](/channels/rcs)                        |
| Viber    | `POST /messages/viber`    | [Viber channel](/channels/viber)                    |
| Email    | `POST /messages/email`    | [Email channel](/channels/email)                    |

## 2. Track delivery

A queued message moves through a status lifecycle before it reaches the recipient:

```
queued → sending → sent → delivered   (or failed / undelivered)
```

You have two ways to follow it:

**Poll** the message by id:

```bash theme={null}
curl https://api.orbit.devotel.io/api/v1/messages/msg_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6 \
  -H "X-API-Key: dv_test_sk_YOUR_KEY"
```

**Subscribe to webhooks** (recommended — no polling). One event fires per status transition, each carrying the message `id` and new `status`:

* `message.sent` — accepted by the carrier
* `message.delivered` — confirmed delivered to the handset
* `message.failed` — terminal failure (the payload's `status` distinguishes `failed`, `undelivered`, `expired`, and `submitted_no_receipt`; `error_code` / `error_message` carry the provider reason when present)

Register a webhook endpoint once, then let events flow:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/webhooks \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/webhooks/orbit",
    "events": ["message.sent", "message.delivered", "message.failed", "message.received"],
    "secret": "whsec_your_signing_secret"
  }'
```

A delivery event looks like this:

```json theme={null}
{
  "id": "evt_abc123",
  "type": "message.delivered",
  "created_at": "2026-07-20T12:00:00Z",
  "data": {
    "message_id": "msg_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
    "channel": "sms",
    "status": "delivered",
    "is_terminal": true,
    "timestamp": "2026-07-20T12:00:00Z"
  }
}
```

Verify the `X-Orbit-Signature` header before trusting any payload, and deduplicate on the event `id` — delivery is at-least-once. See [Webhooks security](/webhooks/security) for the verification snippet.

## 3. Receive inbound messages

When someone replies to your number (or messages it first), Orbit records an inbound message and — if you subscribed to `message.received` in the step above — POSTs it to your webhook URL:

```json theme={null}
{
  "id": "evt_def456",
  "type": "message.received",
  "created_at": "2026-07-20T12:01:00Z",
  "data": {
    "message_id": "msg_inb_9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c",
    "channel": "sms",
    "from": "+14155552671",
    "to": "+18005551234",
    "body": "STATUS"
  }
}
```

The inbound message is also queryable — list everything you've received with the `direction=inbound` filter:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/messages?direction=inbound&channel=sms" \
  -H "X-API-Key: dv_test_sk_YOUR_KEY"
```

<Note>
  Inbound routing is automatic for numbers you own on Orbit — a reply to any of your sender numbers is captured and, with a `message.received` subscription, delivered to your webhook. No per-number inbound URL wiring is required.
</Note>

## 4. Reply to an inbound message

Replying is just another send, addressed back to the inbound `from`. Swap `to` and `from` and call `POST /messages/sms` again:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/sms \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155552671",
    "from": "+18005551234",
    "body": "Order #1234 is out for delivery, arriving today by 5pm."
  }'
```

Both messages share a conversation, so you can pull the full back-and-forth with the `conversation_id` filter on `GET /messages` once the thread exists.

## Common errors

Every error uses the same shape — match on `error.code` (an UPPERCASE constant), and log `meta.request_id`:

| Code                   | HTTP | Meaning                                                 | Fix                                     |
| ---------------------- | ---- | ------------------------------------------------------- | --------------------------------------- |
| `INVALID_API_KEY`      | 401  | Key revoked, wrong environment, or typo                 | Check **Settings → API Keys**           |
| `INVALID_PHONE_NUMBER` | 422  | `to` is not valid E.164 or not reachable                | Validate the number before sending      |
| `NOT_SMS_CAPABLE`      | 422  | Sender isn't SMS-capable (e.g. toll-free without 10DLC) | Register or swap the number             |
| `INSUFFICIENT_BALANCE` | 402  | Wallet below the channel minimum                        | Top up your wallet                      |
| `RATE_LIMITED`         | 429  | Too many sends                                          | Respect `details.retry_after` (seconds) |
| `VALIDATION_ERROR`     | 422  | Body shape wrong                                        | Read `details.issues` for each field    |

Full list: [Error Codes](/reference/error-codes).

## Next steps

* [Messaging API reference](/api-reference/endpoints/messaging) — every message endpoint and field
* [Message status lifecycle](/api-reference/messages-status-lifecycle) — the full status set per channel
* [Webhooks overview](/webhooks/overview) — retries, delivery guarantees, and the event catalog
* [API Integration](/guides/api-integration) — sandbox, idempotency, pagination, and SDKs across every channel
* [Rate Limits](/guides/rate-limits) — per-channel limits and retry headers
