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

# PHP SDK: Orbit quickstart for PHP

> Official Orbit PHP SDK quickstart — messaging, voice, contacts, campaigns, and OTP verify with copy-pasteable examples.

# PHP SDK

The Orbit PHP SDK wraps the platform's core API resources — messaging (SMS, WhatsApp, email), voice, contacts, campaigns, verify (OTP), and webhook signature verification — with typed clients. It requires PHP 8.1+ with `ext-curl`, `ext-json`, and `ext-openssl`.

<Note>
  **Pre-publish — source-only.** This SDK is not yet on Packagist —
  any `composer require` against it fails today. Until first publish,
  vendor the source from the monorepo (`packages/sdk-php/`) or call the
  [REST API](/api-reference) directly. See
  [PHP: core-scope, not full parity](/sdks#php-core-scope-not-full-parity)
  on the SDK index for exactly what is and isn't wrapped, and the low-level
  `$client->request(method: ..., path: ..., ...)` escape hatch for uncovered routes
  (worked example [below](#covered-route-missing-use-the-escape-hatch)).
</Note>

## Installation

Not installable from Packagist yet. Vendor the SDK source from the monorepo
(`packages/sdk-php/`) via a Composer path repository, or call the REST API
directly with any HTTP client until the first release ships (coordinates change).

## Client initialization

```php theme={null}
<?php
require __DIR__.'/vendor/autoload.php';

use Devotel\Orbit\OrbitClient;

$client = OrbitClient::fromApiKey('dv_live_sk_...');
```

Or read the key from an environment variable (`ORBIT_API_KEY`):

```php theme={null}
$client = OrbitClient::fromEnv();
```

Tune timeouts and retries with the direct constructor:

```php theme={null}
$client = new OrbitClient(
    apiKey: 'dv_live_sk_...',
    baseUrl: 'https://api.orbit.devotel.io/api/v1',
    timeoutSeconds: 30.0,
    maxRetries: 3,
    initialBackoffSeconds: 1.0,
);
```

## Quickstart: send your first SMS

A runnable end-to-end — the key comes from `ORBIT_API_KEY`, never from
source. Copy it into `first-send.php` and run it with `php first-send.php`:

```php theme={null}
<?php
require __DIR__.'/vendor/autoload.php';

use Devotel\Orbit\OrbitClient;

$client = OrbitClient::fromEnv();  // reads ORBIT_API_KEY

$result = $client->messages->sendSms('+14155552671', 'Hello from Orbit!');

echo 'message id: '.$result['data']['id'].PHP_EOL;      // msg_abc123
echo 'status: '.$result['data']['status'].PHP_EOL;      // 'queued' — watch it reach 'delivered'
echo 'status URL: /api/v1/messages/'.$result['data']['id'].PHP_EOL;
```

Point `ORBIT_API_KEY` at a sandbox key prefixed `dv_test_sk_` first —
sandbox sends are simulated, free, and never reach a carrier. Swap in your
live key (`dv_live_sk_...`) when you're ready to send for real; the code
does not change.

The response shape above comes from the
[Messages API reference](/api-reference/endpoints/messaging) — its language
tabs include this exact call.

## Messaging

```php theme={null}
$result = $client->messages->sendSms(
    to: '+14155552671',
    body: 'Hello from Orbit!',
);
echo $result['data']['id'], PHP_EOL;

$client->messages->sendWhatsApp(to: '+14155552671', body: 'Hello on WhatsApp!');
$client->messages->sendEmail(
    to: 'user@example.com',
    subject: 'Welcome',
    body: 'Thanks for signing up.',
);
```

## Voice

```php theme={null}
// Place an outbound call, then hang up.
$call = $client->voice->createCall(['to' => '+14155552671', 'from' => '+14155550000']);
$client->voice->hangup($call['data']['id']);
```

## Verify (OTP)

The send-and-check round trip is the most common first integration on the
platform — here as a complete flow. Send the code, keep the returned
verification id, and check the code the user typed in against it:

```php theme={null}
// 1. Send an OTP over SMS.
$sent = $client->verify->send('+14155552671');

// 2. Check the code the user typed in, against the id send() returned.
$result = $client->verify->check($sent['data']['id'], '482901');

echo 'valid: '.($result['data']['valid'] ? 'true' : 'false').PHP_EOL;   // true
echo 'status: '.$result['data']['status'].PHP_EOL;                       // 'approved'
```

A wrongly-typed or expired code reports `valid: false` — it never throws
for a bad guess (only for transport/auth failures), so branch on the flag.
The [Verify API reference](/api-reference/endpoints/verify) covers the full
contract, and [Starter examples](/guides/starter-examples) ships a complete
OTP sign-in starter repo (`orbit-otp-nextjs`).

## Contacts

```php theme={null}
// Upsert a contact and tag it.
$contact = $client->contacts->create(['phone' => '+14155552671', 'first_name' => 'Ada']);
$client->contacts->addTags($contact['data']['id'], ['vip']);
```

`$client->contacts` also exposes `get`, `update`, and `delete` by id.

## Campaigns

```php theme={null}
// Create and trigger a send.
$campaign = $client->campaigns->create(['name' => 'Launch', 'channel' => 'sms']);
$client->campaigns->send($campaign['data']['id']);
```

## Paginate a list

List endpoints are cursor-paginated — read `meta.pagination.cursor` and
`meta.pagination.has_more` off each response and pass the cursor back as a
query param until `has_more` is false. Page through SMS messages with the
escape hatch:

```php theme={null}
$query = ['channel' => 'sms', 'limit' => 100];
$total = 0;

do {
    $page = $client->request('GET', '/messages', $query);

    $total += count($page['data']);
    $pagination = $page['meta']['pagination'];
    $hasMore = $pagination['has_more'];

    if ($hasMore) {
        $query['cursor'] = $pagination['cursor'];
    }
} while ($hasMore);

echo "Fetched {$total} messages", PHP_EOL;
```

The full pagination model (cursor vs. offset endpoints, page-size caps, and
why cursors are not bookmarkable) is in the
[Pagination guide](/guides/pagination).

## Error handling

All Orbit-originated errors inherit from `Devotel\Orbit\Errors\OrbitError`:

| Exception                  | Raised when                                                |
| -------------------------- | ---------------------------------------------------------- |
| `OrbitAuthenticationError` | 401/403 — bad or missing-scope API key                     |
| `OrbitClientError`         | other 4xx (invalid request)                                |
| `OrbitRateLimitError`      | 429 after retries exhausted; check `$e->retryAfter`        |
| `OrbitServerError`         | 5xx after retries exhausted, or persistent network failure |
| `OrbitError`               | base class — catch to handle any Orbit-originated error    |

```php theme={null}
use Devotel\Orbit\Errors\{
    OrbitError,
    OrbitAuthenticationError,
    OrbitClientError,
    OrbitRateLimitError,
    OrbitServerError,
};

try {
    $client->messages->sendSms(to: '+14155552671', body: '...');
} catch (OrbitRateLimitError $e) {
    // throttled — retry in $e->retryAfter seconds
} catch (OrbitAuthenticationError $e) {
    // bad API key — rotate
} catch (OrbitError $e) {
    error_log("orbit: {$e->errorCode} ({$e->statusCode}) — {$e->getMessage()}");
}
```

Every non-GET request automatically carries an `Idempotency-Key` header (UUIDv4); override it per call with your own stable key (`idempotencyKey: 'job-7a3b9d-attempt-1'`).

## Covered route missing? Use the escape hatch

The typed clients wrap 8 core resources; the rest of the API — contact segments, event sinks, frequency caps, and everything else listed as out of scope on the [SDK index](/sdks#php-core-scope-not-full-parity) — is reachable through `$client->request(method, path, ...)`. It returns the raw JSON body as an array. Fetch a segment by id:

```php theme={null}
$segment = $client->request('GET', '/contacts/segments/seg_01hxyz');
echo $segment['data']['name'], PHP_EOL;
```

The escape hatch carries the same auth, retry, and error model as the typed clients — treat it as a first-class client, not a fallback cURL call.

## Webhook signature verification

```php theme={null}
use Devotel\Orbit\Webhooks;
use Devotel\Orbit\Errors\OrbitWebhookSignatureError;

$body = file_get_contents('php://input');
$sig  = $_SERVER['HTTP_X_ORBIT_SIGNATURE'] ?? '';

try {
    $event = Webhooks::verify(
        payload: $body,
        signature: $sig,
        secret: getenv('ORBIT_WEBHOOK_SECRET'),
    );
} catch (OrbitWebhookSignatureError $e) {
    // Forgery attempt — drop, do NOT respond 200.
    http_response_code(400);
    exit;
}

// $event is an associative array — match on $event['type'], etc.
```

Signatures use the `t=<unix_ts>,v1=<hex_hmac>` format (same as Stripe) with a 5-minute replay window enforced by default.
