PHPackages                             nateq/sdk - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Mail &amp; Notifications](/categories/mail)
4. /
5. nateq/sdk

ActiveLibrary[Mail &amp; Notifications](/categories/mail)

nateq/sdk
=========

Official PHP SDK for the Nateq API, with first-class Laravel support

v0.1.0(1mo ago)11↓75%MITPHPPHP ^8.2CI passing

Since Jul 17Pushed 1mo agoCompare

[ Source](https://github.com/nateq-ai/sdk-php)[ Packagist](https://packagist.org/packages/nateq/sdk)[ Docs](https://github.com/nateq-ai/sdk-php)[ RSS](/packages/nateq-sdk/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (4)Versions (2)Used By (0)

Nateq PHP SDK
=============

[](#nateq-php-sdk)

[![Packagist Version](https://camo.githubusercontent.com/75ad6aa6a428cede19ad5a1ccdfaf920d91fe6e4d3e7f98542ce33ca0ffca338/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6e617465712f73646b2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/nateq/sdk)[![PHP Version](https://camo.githubusercontent.com/bf4fcbd498f725381873e945fa3b5a14158a9c8cff921371c5bd9b8eb109874a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646570656e64656e63792d762f6e617465712f73646b2f7068702e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/nateq/sdk)[![CI](https://camo.githubusercontent.com/41692f6d31cfe046dd0a7df5dd31f0b3a4b4a649710cef7a959dedd3adf38c94/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6e617465712d61692f73646b2d7068702f63692e796d6c3f6272616e63683d6d61696e267374796c653d666c61742d737175617265266c6162656c3d6369)](https://github.com/nateq-ai/sdk-php/actions)[![License](https://camo.githubusercontent.com/7b5307e3e3f71866c40faefeb754914a1c01fa88cfce3732fa5a8370c7228246/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6e617465712f73646b2e7376673f7374796c653d666c61742d737175617265)](LICENSE)

The official PHP client for the [Nateq](https://nateq.io) API, with first-class Laravel support. Send transactional email from your application and look up what happened to it.

Zero required dependencies, typed results, and an HTTP layer you can swap out in tests.

> **This is a server-side SDK.** Your API key carries your organization's full scope grant. Never ship it to a browser, a mobile app, a Blade-rendered `` block, or any response body. Keep it in `.env`, out of version control, and out of your logs.

Contents
--------

[](#contents)

- [Requirements](#requirements)
- [Install](#install)
- [Quick start](#quick-start)
- [Authentication](#authentication)
- [Sending email](#sending-email)
- [Reading email](#reading-email)
- [Errors](#errors)
- [Retries and duplicate sends](#retries-and-duplicate-sends)
- [Laravel](#laravel)
- [Other frameworks](#other-frameworks)
- [Testing](#testing)
- [Contributing](#contributing)
- [License](#license)

Requirements
------------

[](#requirements)

- PHP 8.2 or newer
- `ext-curl` and `ext-json`
- Laravel 11 or 12 (optional — the SDK works without it)

PHP 8.1 and Laravel 10 are not supported: both left security support before this package existed. The test suite runs on PHP 8.2, 8.3, and 8.4 against Laravel 12.

Install
-------

[](#install)

```
composer require nateq/sdk
```

In Laravel the service provider and `Nateq` facade are auto-discovered. Nothing to register.

Quick start
-----------

[](#quick-start)

```
use Nateq\Sdk\Nateq;

$nateq = new Nateq(); // reads NATEQ_API_KEY

$result = $nateq->emails()->send(
    toEmails: ['customer@example.com'],
    subject: 'Welcome aboard',
    htmlBody: 'Thanks for signing up.',
);

echo $result->id; // "9f8c…" — use this to look the email up later
```

In Laravel, resolve the client instead of constructing it:

```
use Nateq\Sdk\Nateq;

class WelcomeMailer
{
    public function __construct(private Nateq $nateq) {}

    public function send(User $user): void
    {
        $this->nateq->emails()->send(
            toEmails: [$user->email],
            subject: 'Welcome aboard',
            htmlBody: view('mail.welcome', ['user' => $user])->render(),
        );
    }
}
```

Authentication
--------------

[](#authentication)

Create an API key in the dashboard under **Settings → API keys**. It needs:

ScopeGrants`emails:send`Sending mail`emails:read`Looking sends up (`get`, `list`)Put it in your environment:

```
NATEQ_API_KEY=tg_live_…
```

The SDK reads `NATEQ_API_KEY` by default. To pass it explicitly — from a secret manager, or when you talk to more than one organization:

```
$nateq = new Nateq(apiKey: $secrets->get('nateq_api_key'));
```

The key is validated locally before any request, so a mistyped key fails immediately instead of being sent over the network. It travels in the `Authorization` header only, never in a URL where it would land in access logs and `Referer` headers.

**The client will not leak the key when rendered.** `var_dump`, `json_encode`, and framework error pages all show it redacted, and serializing a client throws rather than writing your key into a session, cache entry, or queue payload:

```
var_dump($nateq);
// object(Nateq\Sdk\Nateq) { ["baseUrl"]=> "https://api.nateq.io/api/v1"
//                           ["apiKey"]=> "[REDACTED:…ghij]" }
```

> `print_r()` and `var_export()` have no redaction hook in PHP and **will** print private properties verbatim. Don't pass the client to either.

Sending email
-------------

[](#sending-email)

`send()` returns once the API has accepted the message. Delivery happens afterwards — a successful return means Nateq took responsibility for it, not that it landed.

```
$result = $nateq->emails()->send(
    toEmails: ['customer@example.com'],
    subject: 'Your receipt',
    htmlBody: 'Thanks!',
    plainTextBody: 'Thanks!',
);

$result->id;                // string
$result->status;            // EmailStatus::Pending
$result->providerMessageId; // ?string
```

Every option, all optional except `toEmails` and `subject`:

```
$nateq->emails()->send(
    toEmails: ['a@example.com', 'b@example.com'],
    subject: 'Quarterly update',
    htmlBody: 'Hello',
    plainTextBody: 'Hello',
    fromEmail: 'hello@yourdomain.com',   // must be a verified org address
    fromName: 'Acme Support',
    emailAddressId: '…',                 // or pick the verified address by id
    ccEmails: ['c@example.com'],
    bccEmails: ['d@example.com'],
    replyTo: 'support@yourdomain.com',
    inReplyTo: '',         // threading
    references: '',
    ticketId: '…',                       // link the send to a ticket…
    conversationId: '…',                 // …a conversation…
    contactId: '…',                      // …or a contact
    attachmentIds: ['…'],                // ids of previously uploaded files
    headers: ['X-Campaign' => 'q3'],
);
```

Provide `htmlBody`, `plainTextBody`, or both. Omit `fromEmail` and `emailAddressId` and your organization's default verified address is used.

Reading email
-------------

[](#reading-email)

Look up a single send, including its delivery status:

```
$email = $nateq->emails()->get($result->id);

$email->status;       // EmailStatus::Delivered
$email->openCount;    // int
$email->bounceReason; // ?string
```

`EmailStatus` is an enum with helpers, so you don't have to memorise which of the nine statuses count as success:

```
if ($email->status->isFailure()) {   // bounced | failed | rejected
    Log::warning('Email did not arrive', ['reason' => $email->bounceReason]);
}

$email->status->isDelivered(); // delivered | opened | clicked
$email->status->isPending();   // pending | sending | sent
```

List sends, newest first. The result is countable and iterable:

```
use Nateq\Sdk\Types\EmailStatus;

$page = $nateq->emails()->list(
    status: EmailStatus::Bounced,
    toEmail: 'customer@example.com',
    limit: 25,
);

foreach ($page as $email) {
    echo $email->subject;
}

$page->total;        // total across all pages
$page->hasMore();    // bool
$page->nextOffset(); // ?int — feed straight back into list(offset: …)
```

Paging through everything:

```
$offset = 0;
do {
    $page = $nateq->emails()->list(limit: 100, offset: $offset);
    foreach ($page as $email) {
        // …
    }
    $offset = $page->nextOffset();
} while ($offset !== null);
```

`limit` defaults to 50 and is capped at 100 server-side.

The API returns more fields than the SDK models. Anything not promoted to a property is still available on `$email->raw`, so a newly added field works without an SDK upgrade.

Errors
------

[](#errors)

Everything the SDK throws extends `NateqException`, so one `catch` covers the surface:

```
use Nateq\Sdk\Exceptions\NateqException;
use Nateq\Sdk\Exceptions\PermissionException;
use Nateq\Sdk\Exceptions\RateLimitException;
use Nateq\Sdk\Exceptions\ValidationException;

try {
    $nateq->emails()->send(/* … */);
} catch (ValidationException $e) {
    // 400/422 — the request was wrong. Don't retry it unchanged.
} catch (PermissionException $e) {
    // 403 — key lacks emails:send, endpoint isn't public to keys, or IP not allowed.
    $e->errorCode; // e.g. "INSUFFICIENT_SCOPE"
} catch (RateLimitException $e) {
    // 429 — nothing was sent.
    $e->retryAfter; // ?float, seconds
} catch (NateqException $e) {
    $e->status;    // ?int  HTTP status
    $e->errorCode; // ?string machine-readable code
    $e->details;   // mixed  validation details, scope lists, …
    $e->requestId; // ?string — quote this to support
}
```

ExceptionWhen`ValidationException`400/422, or bad arguments caught before any request`AuthenticationException`401 — key missing, malformed, revoked, unknown`PermissionException`403 — key valid, but not allowed to do this`NotFoundException`404`RateLimitException`429 — carries `retryAfter``ServerException`5xx`TimeoutException`exceeded the configured timeout`ConnectionException`DNS, TLS, socket, offline`ConfigurationException`bad SDK setup — thrown before any requestNo exception ever carries your API key: every message is scrubbed before it is raised, because framework error pages and log aggregators render these by default.

Retries and duplicate sends
---------------------------

[](#retries-and-duplicate-sends)

Reads (`get`, `list`) are retried automatically on timeouts, 5xx, and connection failures, with full-jitter exponential backoff that honours `Retry-After`.

**`send()` is deliberately not.** The API has no idempotency key, so a send that fails *after* reaching the server may already have gone out. Replaying it could mail your customer twice, and the SDK will not make that decision for you.

The one exception is a **429**, which the API raises during validation before anything is sent — so it is retried, because a duplicate is impossible.

If a `send()` throws `TimeoutException` or `ServerException`, the outcome is genuinely unknown. Look it up before retrying:

```
use Nateq\Sdk\Exceptions\ServerException;
use Nateq\Sdk\Exceptions\TimeoutException;

try {
    $nateq->emails()->send(toEmails: [$user->email], subject: $subject, htmlBody: $html);
} catch (TimeoutException|ServerException $e) {
    // Might have sent. Check before trying again.
    $recent = $nateq->emails()->list(toEmail: $user->email, limit: 5);
    // …decide based on what's there
}
```

Tune retries with `maxRetries` (`0` disables them):

```
$nateq = new Nateq(timeout: 10.0, maxRetries: 0);
```

Laravel
-------

[](#laravel)

The package auto-discovers. Publish the config only if you want to edit it:

```
php artisan vendor:publish --tag=nateq-config
```

```
// config/nateq.php
return [
    'api_key' => env('NATEQ_API_KEY'),
    'base_url' => env('NATEQ_BASE_URL', 'https://api.nateq.io/api/v1'),
    'timeout' => env('NATEQ_TIMEOUT', 30),
    'max_retries' => env('NATEQ_MAX_RETRIES', 2),
];
```

Inject the client, or use the facade:

```
use Nateq\Sdk\Laravel\Facades\Nateq;

Nateq::emails()->send(
    toEmails: ['customer@example.com'],
    subject: 'Welcome aboard',
    htmlBody: 'Thanks for signing up.',
);
```

**Send from a queued job, not from the request.** Email is a network call to a third party; doing it inline ties your response time to ours and turns our bad minute into your timeout.

```
class SendWelcomeEmail implements ShouldQueue
{
    use Queueable;

    public function __construct(public User $user) {}

    // Do not retry blindly: the send may have succeeded before the failure.
    public $tries = 1;

    public function handle(Nateq $nateq): void
    {
        $nateq->emails()->send(
            toEmails: [$this->user->email],
            subject: 'Welcome aboard',
            htmlBody: view('mail.welcome', ['user' => $this->user])->render(),
        );
    }
}
```

Resolve the client via the `handle()` signature as above — never store it on the job as a property. Job properties are serialized into the queue payload, and the client refuses to serialize precisely so your API key can't end up sitting in Redis in plain text.

If you change `NATEQ_API_KEY` and nothing seems to happen, you have a cached config:

```
php artisan config:clear
```

Other frameworks
----------------

[](#other-frameworks)

The core has no framework ties — construct `Nateq` and go. In Symfony, register it as a service:

```
# config/services.yaml
services:
    Nateq\Sdk\Nateq:
        arguments:
            $apiKey: '%env(NATEQ_API_KEY)%'
```

To route traffic through your own HTTP stack (an outbound proxy, Guzzle, Symfony HttpClient), implement `HttpClient`:

```
use Nateq\Sdk\Http\{HttpClient, Request, Response};

final class GuzzleHttpClient implements HttpClient
{
    public function __construct(private \GuzzleHttp\Client $guzzle) {}

    public function send(Request $request, float $timeoutSeconds): Response
    {
        $res = $this->guzzle->request($request->method, $request->url, [
            'headers' => $request->headers,
            'body' => $request->body,
            'timeout' => $timeoutSeconds,
            'http_errors' => false, // the SDK maps statuses itself
        ]);

        return new Response(
            $res->getStatusCode(),
            array_map(fn ($v) => $v[0], $res->getHeaders()),
            (string) $res->getBody(),
        );
    }
}

$nateq = new Nateq(httpClient: new GuzzleHttpClient($guzzle));
```

Testing
-------

[](#testing)

Don't hit the API from your test suite. Implement `HttpClient` with a fake and inject it:

```
use Nateq\Sdk\Http\{HttpClient, Request, Response};
use Nateq\Sdk\Nateq;

$fake = new class implements HttpClient {
    public array $requests = [];

    public function send(Request $request, float $timeoutSeconds): Response
    {
        $this->requests[] = $request;

        return new Response(201, [], json_encode([
            'id' => 'test-id', 'status' => 'pending', 'createdAt' => 'now',
        ]));
    }
};

$nateq = new Nateq(apiKey: 'tg_test_…', httpClient: $fake);
```

In Laravel, bind it and the container-resolved client picks it up — facade included:

```
$this->app->instance(HttpClient::class, $fake);

Nateq::emails()->send(toEmails: ['a@example.com'], subject: 'Hi', htmlBody: 'Hi');

$this->assertCount(1, $fake->requests);
```

Contributing
------------

[](#contributing)

```
composer install
composer test    # phpunit
composer stan    # phpstan, level 6
composer fmt     # php-cs-fixer
composer check   # all three
```

License
-------

[](#license)

[MIT](LICENSE)

###  Health Score

35

—

LowBetter than 77% of packages

Maintenance90

Actively maintained with recent releases

Popularity3

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity36

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

46d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/8060245?v=4)[nateq](/maintainers/nateq)[@nateq](https://github.com/nateq)

---

Top Contributors

[![2hmad](https://avatars.githubusercontent.com/u/32125808?v=4)](https://github.com/2hmad "2hmad (9 commits)")

---

Tags

laravelpackagephpsdksdk-phpapilaravelsdkemailnateq

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/nateq-sdk/health.svg)

```
[![Health](https://phpackages.com/badges/nateq-sdk/health.svg)](https://phpackages.com/packages/nateq-sdk)
```

###  Alternatives

[hafael/azure-mailer-driver

Supercharge your Laravel or Symfony app with Microsoft Azure Communication Services (ACS)! Effortlessly add email, chat, voice, video, and telephony-over-IP for next-level communication. 🚀

15146.2k](/packages/hafael-azure-mailer-driver)[hocza/sendy

Sendy API implementation for Laravel

74213.3k](/packages/hocza-sendy)[princealikhan/laravel-mautic-api

Free and Open Source Marketing Automation API

405.9k](/packages/princealikhan-laravel-mautic-api)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
