PHP integration: send transactional email

Updated

An official SDK is available: inboxili/inboxili on Packagist. The sections below also show a plain cURL client, if you prefer your own.

PHP applications often send mail with mail() or PHPMailer over SMTP. Inboxili is API-only, so the integration is an HTTP call. This client keeps it tidy.

Use the official SDK

composer require inboxili/inboxili
use Inboxili\Client;

$inboxili = new Client(getenv('INBOXILI_API_KEY'));

$result = $inboxili->emails->send([
    'to' => 'ada@example.com',
    'from_email' => 'hello@yourdomain.com',
    'subject' => 'Welcome, {{first_name}}',
    'html_body' => '<p>Hi {{first_name}}, your account is ready.</p>',
    'template_data' => ['first_name' => 'Ada'],
]);

Requires PHP 8.1 or later with the curl and json extensions. It throws InboxiliException and ConnectionException, retries HTTP 429 only, rejects unknown fields instead of dropping them, and includes Webhook::verify. Source: github.com/inboxili/inboxili-php. Package: inboxili/inboxili on Packagist.

Or write your own client

Requirements

  • PHP 8.0 or later with curl and json
  • An Inboxili API key with the transactional:send scope
  • A verified sending domain

Set the key in the environment, for example in your web server or container config: INBOXILI_API_KEY=ik_live_....

The client

Save as Inboxili.php:

<?php
declare(strict_types=1);

final class InboxiliException extends RuntimeException
{
    public function __construct(public readonly int $status, public readonly string $errorCode, string $message)
    {
        parent::__construct($message);
    }
}

final class Inboxili
{
    public function __construct(
        private string $apiKey,
        private string $baseUrl = 'https://api.inboxili.com/api/v1',
        private int $timeout = 10,
    ) {
    }

    /** @return array{status: string, message_id: ?string} */
    public function send(array $message): array
    {
        $payload = array_filter($message, static fn ($v) => $v !== null);

        $ch = curl_init($this->baseUrl . '/transactional/send');
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => $this->timeout,
            CURLOPT_HTTPHEADER => [
                'Authorization: Bearer ' . $this->apiKey,
                'Content-Type: application/json',
            ],
            CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR),
        ]);
        $raw = curl_exec($ch);
        $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        $curlError = curl_error($ch);
        curl_close($ch);

        if ($raw === false) {
            throw new InboxiliException(0, 'network_error', $curlError ?: 'No response from Inboxili');
        }

        $body = json_decode($raw, true) ?? [];
        if ($status !== 200) {
            throw new InboxiliException(
                $status,
                $body['error']['code'] ?? 'http_error',
                $body['error']['message'] ?? "HTTP $status",
            );
        }

        return $body;
    }
}

Field names are the API's own snake_case names (from_email, html_body, template_data), passed straight through.

Send an email

<?php
require 'Inboxili.php';

$inboxili = new Inboxili(getenv('INBOXILI_API_KEY'));

try {
    $result = $inboxili->send([
        'to' => 'ada@example.com',
        'from_email' => 'hello@yourdomain.com',
        'from_name' => 'Acme',
        'subject' => 'Welcome, {{first_name}}',
        'html_body' => '<p>Hi {{first_name}}, your account is ready.</p>',
        'template_data' => ['first_name' => 'Ada'],
    ]);
    echo 'sent ' . $result['message_id'] . PHP_EOL;
} catch (InboxiliException $e) {
    if ($e->errorCode === 'sender_not_verified') {
        error_log('Verify the sending domain first: ' . $e->getMessage());
    }
    throw $e;
}

Send from a template

$inboxili->send([
    'to' => 'ada@example.com',
    'from_email' => 'hello@yourdomain.com',
    'template_id' => '00000000-0000-0000-0000-000000000000',
    'template_data' => ['first_name' => 'Ada'],
]);

Error handling

| status | errorCode | Do | |---|---|---| | 0 | network_error | Outcome unknown. Do not auto-retry. | | 401 | unauthorized | Check the key. | | 403 | forbidden | Check scope and IP rules. | | 422 | validation_error, sender_not_verified, send_failed | Fix the request or inspect the message. | | 429 | rate_limited | Wait and retry. |

There is no idempotency key. After a timeout the email may still have been sent, so record "sent" state in your own database for anything that must not be duplicated.

Testing

Wrap the client behind an interface in your app and fake it in tests. To test the client itself, point baseUrl at a local stub server:

<?php
use PHPUnit\Framework\TestCase;

final class InboxiliTest extends TestCase
{
    public function testMapsApiErrors(): void
    {
        // php -S 127.0.0.1:8099 stub.php, where stub.php returns:
        // http_response_code(422); echo json_encode(['error' => ['code' => 'sender_not_verified', 'message' => 'no']]);
        $client = new Inboxili('ik_live_test', 'http://127.0.0.1:8099');

        try {
            $client->send(['to' => 'a@b.co', 'from_email' => 'x@y.co', 'subject' => 's', 'html_body' => '<p>x</p>']);
            $this->fail('expected exception');
        } catch (InboxiliException $e) {
            $this->assertSame('sender_not_verified', $e->errorCode);
        }
    }
}

Production notes

  • Never put the key in the repository. Use environment variables or a secret manager.
  • Do not send inside a web request path where latency matters. Use a queue or a deferred job.
  • Verify webhooks. The X-Inboxili-Signature header is the hex HMAC-SHA256 of the raw request body:
function verifyInboxiliWebhook(string $rawBody, string $signatureHex, string $secret): bool
{
    return hash_equals(hash_hmac('sha256', $rawBody, $secret), $signatureHex);
}

Read the body with file_get_contents('php://input') so you hash the exact bytes received.

Frequently asked questions

Do I need Composer packages?
No. PHP 8 with the curl and json extensions is enough. Guzzle works too if you already use it.
Can I use PHP's mail() function with Inboxili?
No. Inboxili has no SMTP endpoint, so mail() and any SMTP-only library cannot send through it. Call the API instead.

Build with Inboxili

Create a workspace, verify a domain, and make your first API call.

Related