# PHP SDK

> Send email and manage contacts from PHP with the official Nexiom Connect SDK.

`nexiom/connect-php` is the official SDK for PHP. It works in Laravel, Symfony, WordPress, or plain PHP, and retries safely for you.

## Install

PHP 8.2 or later is required. The SDK uses Guzzle 7.9 or 8.

```shell
composer require nexiom/connect-php
```

## Initialize the client

Create one client on your server and reuse it. Load the API key from your environment; never ship it to the browser.

```php
<?php

use Nexiom\Connect\NexiomConnect;

$apiKey = getenv('NEXIOM_API_KEY');
if (!$apiKey) {
    throw new RuntimeException('NEXIOM_API_KEY is required');
}

$nexiom = new NexiomConnect($apiKey);
```

| Parameter | Type | Description |
| --- | --- | --- |
| `apiKey` | string · required | Your API key, starting with `nc_`. |
| `timeout` | int or float · optional | Total deadline per call in seconds, including retries. Default `30`. |
| `maxRetries` | int · optional | Extra attempts for reads, sends, and cancels. Default `2`. |
| `baseUrl` | string · optional | API root. Default `https://api-connect.nxiom.com/api`. |
| `httpClient` | GuzzleHttp\ClientInterface · optional | A Guzzle client, for proxies, instrumentation, or tests. |

Pass options by name, for example `new NexiomConnect(apiKey: $apiKey, timeout: 10)`.

## Send an email

Parameters are arrays with camelCase keys. Methods return the response data as an array.

```php
<?php

$email = $nexiom->emails->send(
    [
        'from' => 'hello@mail.example.com',
        'fromName' => 'Acme',
        'to' => 'reader@example.com',
        'subject' => 'Your receipt',
        'html' => '<p>Thanks for your order.</p>',
        'text' => 'Thanks for your order.',
    ],
    ['idempotencyKey' => 'receipt-' . $order->id],
);

echo $email['messageId'];
```

Pass an `idempotencyKey` that identifies the email in your system. If you omit it, the SDK generates one per call, which protects its own retries but not a retry of your whole operation. See [Sending email](/docs/learn/sending#idempotency-keys).

To send a template, pass `templateId` and `templateVariables` instead of `subject` and content.

## Schedule an email

Pass `scheduledAt` as a `DateTimeInterface` or an ISO 8601 timestamp with an offset, up to 30 days ahead. Keep the returned `messageId` to move or cancel the send before it goes out.

```php
<?php

$email = $nexiom->emails->send([
    'from' => 'hello@mail.example.com',
    'to' => 'reader@example.com',
    'subject' => 'Your trial ends soon',
    'text' => 'Your trial ends soon.',
    'scheduledAt' => new DateTimeImmutable('+1 day'),
]);

// Move it one day later, or cancel it before it is sent.
$nexiom->emails->reschedule($email['messageId'], [
    'scheduledAt' => new DateTimeImmutable('+2 days'),
]);
$nexiom->emails->cancel($email['messageId']);
```

See [Reschedule](/docs/api/emails/reschedule) and [Cancel](/docs/api/emails/cancel) for the rules.

## Read email logs

`emails->list` and `emails->get` need a full-access key. `list` pages with a cursor; `get` takes a delivery ID, not a message ID.

```php
<?php

$page = $nexiom->emails->list(['status' => 'bounced', 'limit' => 50]);

$first = $page['items'][0] ?? null;

if ($first !== null) {
    $delivery = $nexiom->emails->get($first['id']);

    echo $delivery['recipient'], ' ', $delivery['delivery_reason'];
}

// nextCursor is null on the last page.
if ($page['nextCursor'] !== null) {
    $nexiom->emails->list(['status' => 'bounced', 'cursor' => $page['nextCursor']]);
}
```

See [List emails](/docs/api/emails/list) and [Retrieve an email](/docs/api/emails/retrieve) for every field.

## Manage contacts

Define a contact property before you store a value for it.

```php
<?php

$nexiom->contacts->properties->create(['name' => 'plan', 'type' => 'string']);

$contact = $nexiom->contacts->create([
    'email' => 'reader@example.com',
    'firstName' => 'Sam',
    'properties' => ['plan' => 'pro'],
]);

$nexiom->contacts->update($contact['id'], [
    'email' => 'reader@example.com',
    'emailStatus' => 'unsubscribed',
]);

$page = $nexiom->contacts->list(['search' => 'example.com', 'limit' => 50]);
```

`contacts->get`, `contacts->delete`, and `contacts->properties` are available too. Responses use snake_case fields such as `first_name`. See the [Contacts API](/docs/api/contacts/create) for every field.

## Send a WhatsApp message

`messaging->send` sends on every messaging channel; `channel` selects the parameters. For WhatsApp, send an approved template from your connected number, named by its phone number in `from`. Requires 0.4.0 or later and a key with the `whatsapp.send` permission.

```php
$message = $nexiom->messaging->send(
    [
        'channel' => 'whatsapp',
        'from' => '+213555000111',
        'to' => '+213555123456',
        'template' => [
            'id' => 'wat_example',
            'variables' => ['body' => [1 => 'Amina', 2 => 'A-1234']],
        ],
    ],
    ['idempotencyKey' => 'order-A-1234-shipped'],
);

$current = $nexiom->messaging->get($message['id']);

echo $current['status'];
```

See [Sending WhatsApp messages](/docs/whatsapp/sending).

## Verify webhooks

`webhooks->constructEvent` (0.4.0 or later) checks a webhook request's signature and timestamp, and returns the event as an array. Pass the raw body, not decoded JSON. In a Laravel controller or route:

```php
$event = $nexiom->webhooks->constructEvent(
    $request->getContent(),
    $request->headers->all(),
    config('services.nexiom.webhook_secret'),
);

if ($event['type'] === 'email.bounced') {
    error_log($event['data']['recipient']);
}
```

It throws `NexiomWebhookVerificationException` when the request is not genuine. Outside Laravel, pass `file_get_contents('php://input')` and `getallheaders()`. See [Verify webhook requests](/docs/webhooks/verify-webhooks-requests) for a complete Laravel route.

## Handle errors

API and transport failures throw `Nexiom\Connect\Exceptions\NexiomException`.

```php
<?php

use Nexiom\Connect\Exceptions\ErrorKind;
use Nexiom\Connect\Exceptions\NexiomException;

try {
    $email = $nexiom->emails->send($message, ['idempotencyKey' => $idempotencyKey]);
} catch (NexiomException $error) {
    if ($error->kind === ErrorKind::Api && $error->status === 429) {
        // Rate limited: try again later with the same idempotency key.
    }

    error_log("{$error->status} {$error->errorCode} {$error->getMessage()} {$error->requestId}");
    throw $error;
}
```

| Property | Description |
| --- | --- |
| `kind` | `ErrorKind::Api` for an error response; otherwise `Network`, `Timeout`, or `Protocol`. |
| `status` | HTTP status, when the API responded. |
| `errorCode` | Machine-readable error code, when present. |
| `requestId` | The request's correlation ID. Include it when contacting support. |
| `details` | Additional error details, such as validation failures. |

Invalid arguments — for example a missing `from`, or an unknown key such as `from_name` — throw a `NexiomValidationException` before any request is made.

## Retries and timeouts

The SDK retries reads, sends and cancels on `408`, `429`, and `5xx` responses and on network failures, honouring `Retry-After`. Sends are retried with the same idempotency key, so a retry never sends twice. Other writes, such as creating a contact or rescheduling an email, are not retried automatically. If `Retry-After` asks for a longer wait than the remaining timeout, the SDK throws that error straight away.

Override per call by passing `['timeout' => ..., 'maxRetries' => ...]` as the method's last argument.

---

Source: https://connect.nxiom.com/docs/examples/php
