unione-repo / unione-php-sdk
UniOne Web API client library for PHP
Requires
- php: >=8.1
- ext-curl: *
- ext-json: *
Requires (Dev)
- phpunit/phpunit: ^10.5|^11.0
Suggests
- psr/http-client: To send the requests with a PSR-18 client (Guzzle, ...) through Psr18Transport
Provides
None
Conflicts
None
Replaces
None
README
Official PHP library for the UniOne transactional email API.
API reference · Docs for LLMs: llms.txt
On Laravel or Symfony? Install
unione-repo/unione-laravelorunione-repo/unione-symfony-bundleinstead: this SDK plus configuration, a mailer and a webhook route.
Install
composer require unione-repo/unione-php-sdk
The client needs PHP 8.1+ with ext-curl, no other dependencies. A PSR-18 HTTP client (Guzzle, Symfony HttpClient, ...) is optional, see “HTTP client” below.
For PHP 7.2 use the 1.x line (branch 1.x), the same API.
Get an API key in the control panel and keep it in the environment, never in code:
export UNIONE_API_KEY="your-api-key"
Send an email
<?php use UniOne\Api\Email\Model\EmailSendRequest; use UniOne\Api\Email\Model\MessageObject; use UniOne\Api\Email\Model\MessageObjectRecipientsItem as Recipient; use UniOne\Common\Model\BodyObject as Body; use UniOne\UniOne; $client = UniOne::client(getenv('UNIONE_API_KEY')); $response = $client->email()->send(new EmailSendRequest(new MessageObject( recipients: [Recipient::fromAddress('Alex <alex@example.com>')], body: Body::html('<p>Hello, {{to_name}}! Your order has shipped.</p>'), subject: 'Your order', fromEmail: 'shop@example.com', fromName: 'Shop', ))); echo $response->jobId, PHP_EOL;
The sender address must be on a domain verified in your account. No domain yet? Send from the sandbox domain in three steps.
Contents
- Recipes: personalization · attachments · CC and BCC · scheduling · safe retries · errors · webhooks
- No domain yet: the test sandbox domain
- Going to production
- Reference
- Help
Recipes
Every recipe continues the example above: the client and its imports are there.
Personalize for each recipient
Up to 500 recipients per call, each gets a separate copy with its own substitutions. Metadata of a recipient comes back in its webhook events.
$client->email()->send(new EmailSendRequest(new MessageObject( recipients: [ Recipient::fromAddress('Alex <alex@example.com>', ['order' => '1001'])->withMetadata(['user_id' => '42']), Recipient::fromAddress('Maria <maria@example.com>', ['order' => '1002'])->withMetadata(['user_id' => '43']), ], body: Body::html('<p>{{to_name}}, order #{{order}} has shipped.</p>'), subject: 'Order #{{order}}', fromEmail: 'shop@example.com', )));
Attach a file
use UniOne\Common\Model\AttachmentsArrayItem as Attachment; $client->email()->send(new EmailSendRequest(new MessageObject( recipients: [Recipient::fromAddress('alex@example.com')], body: Body::html('<p>Your invoice is attached.</p>'), subject: 'Invoice', fromEmail: 'shop@example.com', attachments: [ new Attachment( type: 'application/pdf', name: 'invoice.pdf', content: base64_encode(file_get_contents('invoice.pdf')), ), ], )));
CC and BCC
Every address goes to recipients (each gets a copy); the visible ones also go to the headers. audit@example.com is in neither header: a BCC. The rules and limits: CC and BCC.
use UniOne\Common\Model\HeadersObject; $client->email()->send(new EmailSendRequest(new MessageObject( recipients: [ Recipient::fromAddress('alex@example.com'), Recipient::fromAddress('boss@example.com'), Recipient::fromAddress('audit@example.com'), ], body: Body::html('<p>The report is attached.</p>'), subject: 'Weekly report', fromEmail: 'shop@example.com', headers: new HeadersObject(extra: ['To' => 'alex@example.com', 'CC' => 'boss@example.com']), )));
Schedule
send_at is UTC, up to 24 hours ahead.
use UniOne\Api\Email\Model\MessageObjectOptions; $sendAt = gmdate('Y-m-d H:i:s', time() + 3600); // UTC, "YYYY-MM-DD hh:mm:ss" $client->email()->send(new EmailSendRequest(new MessageObject( recipients: [Recipient::fromAddress('alex@example.com')], body: Body::text('See you tomorrow!'), subject: 'Reminder', fromEmail: 'shop@example.com', options: new MessageObjectOptions(sendAt: $sendAt), )));
Retry safely
A repeat with the same idempotence_key within a minute is not sent twice: retry the retryable errors (network errors, timeouts, 429 and 5xx) with it; after that minute check your own record of the sent email before repeating.
$client->email()->send(new EmailSendRequest(new MessageObject( recipients: [Recipient::fromAddress('alex@example.com')], body: Body::html('<p>Use this link to reset your password.</p>'), subject: 'Reset your password', fromEmail: 'shop@example.com', idempotenceKey: 'password-reset-8f3a', )));
Handle errors
Recipients the API drops (unsubscribed, invalid, suppressed) are not an error: they are in failed_emails of the response, the rest still get the message.
use UniOne\Client\ApiException; try { $client->email()->send($request); } catch (ApiException $e) { // the message: method, API error code, HTTP status and how to fix it; kind: what failed; retryable: a repeat may succeed echo $e->getMessage(), PHP_EOL; echo $e->getKind(), ' ', $e->isRetryable() ? 'retryable' : 'not retryable', PHP_EOL; }
Receive delivery events
Register your HTTPS endpoint once (webhook/set). Then check the signature of every notification (auth: MD5 of the raw body with the auth value replaced by your API key; a wrong one gets 401) and process the events.
use UniOne\Api\Webhook\WebhookApi; // $raw: the body of the POST request as received; returns the HTTP status to answer function handleWebhook(string $raw): int { $callback = WebhookApi::parseCallback($raw); $auth = (string) $callback->auth; // auth: MD5 of the raw body with the auth value replaced by your API key if (!hash_equals(md5(str_replace($auth, (string) getenv('UNIONE_API_KEY'), $raw)), $auth)) { return 401; } foreach ($callback->eventsByUser ?? [] as $user) { foreach ($user->events ?? [] as $event) { // email, status (delivered, opened, hard_bounced, spam, ...), job_id, metadata } } return 200; } http_response_code(handleWebhook(file_get_contents('php://input')));
Answer 200 within 3 seconds and process the events in the background (a queue): otherwise UniOne repeats the request every 10 minutes for 24 hours (
retry_countgrows), and a handler that failed every request for at least 10 events of a day is turned inactive. The handler must take at least 5 parallel requests of up to 100 events each; deduplicate, a repeat delivers the same events again.
No domain yet: the test sandbox domain
Until your domain is verified, send from the sandbox domain: a test domain every account has, already verified, with DKIM and SPF set. It delivers only to addresses you confirm (the confirmation link comes by email, valid 48 hours), up to 100 emails a day. The sandbox domain is deleted 31 days after it was created, but you can get it again: the next domain/get-sandbox-domain call creates a new one (with a new name, so read it from the call, do not hardcode it).
use UniOne\Api\ConfirmedEmail\Model\ConfirmedEmailRequest; // 1. Your sandbox domain: created on the first call, the same one afterwards $domain = $client->domain()->getSandboxDomain()->domain; // 2. Confirm your inbox: UniOne sends a link, click it within 48 hours $client->confirmedEmail()->create(new ConfirmedEmailRequest(email: 'you@example.com')); // 3. Send from any address on the sandbox domain to the confirmed one $client->email()->send(new EmailSendRequest(new MessageObject( recipients: [Recipient::fromAddress('you@example.com')], body: Body::html('<p>It works!</p>'), subject: 'Hello from UniOne', fromEmail: "noreply@{$domain}", )));
Sandbox limits
- Sends only to confirmed addresses: up to 10 per account.
- Up to 100 emails a day.
- The sandbox domain expires after 31 days; call
domain/get-sandbox-domainagain to get a new one. - Works only with the account API key, not a project key.
- Not listed in
domain/list; the otherdomain/*methods do not apply to it.
The sandbox is for development and testing. Do not use a sandbox domain as the sender in production.
The same with curl
BASE=https://api.unione.io/en/transactional/api/v1 curl -s -X POST $BASE/domain/get-sandbox-domain.json \ -H "Content-Type: application/json" -H "X-API-KEY: $UNIONE_API_KEY" -d '{}' curl -s -X POST $BASE/confirmed-email/create.json \ -H "Content-Type: application/json" -H "X-API-KEY: $UNIONE_API_KEY" \ -d '{"email": "you@example.com"}' curl -s -X POST $BASE/email/send.json \ -H "Content-Type: application/json" -H "X-API-KEY: $UNIONE_API_KEY" \ -d '{ "message": { "recipients": [ { "email": "you@example.com" } ], "from_email": "noreply@<sandbox-domain>", "subject": "Hello from UniOne", "body": { "html": "<p>It works!</p>" } } }'
Going to production
1. Verify your sending domain. domain/get-dns-records returns the verification record, the DKIM key and the DMARC target; add the records at your DNS provider, then domain/validate-verification-record and domain/validate-dkim.
| Type | Host | Value | Where it comes from |
|---|---|---|---|
| TXT | @ |
unione-validate-hash=… |
verification-record of the response, as is |
| TXT | <selector>._domainkey (the selector is on the domain page of the control panel, e.g. us) |
v=DKIM1; k=rsa; p=<dkim> |
dkim of the response after v=DKIM1; k=rsa; p= |
| TXT | @ |
v=spf1 include:spf.unione.io ~all |
not in the response; add the include to your SPF record if you have one |
| CNAME | _dmarc |
<domain>.dmarc.… |
dmarc of the response (UniOne publishes the DMARC policy); keep your own DMARC TXT record instead if the domain has one |
2. Make sending reliable.
| Retries | 429 and 5xx with backoff and the same idempotence_key; fix other errors instead of retrying |
| Limits | 500 recipients and 10 MB per request, attachments included: base64 makes a file about a third larger |
| Body | always html or plaintext (a template can supply it) |
| Delivery status | a webhook, not polling |
| Queue | send from a background job, not inside the web request |
| Config | from_email from config, so the sandbox → production switch needs no code change |
| Defaults | keep the unsubscribe footer and tracking; changing them needs UniOne support |
Reference
Sections
| Section | Methods | Description |
|---|---|---|
send, subscribe |
This group of methods is related with email sending. | |
| EmailValidation | single |
Email validation methods. As a result of the check, you’ll get detailed information on email validity. It will help eliminate nonexistent emails, decrease the number of complaints, clean up the subscribers list and eventually enhance your sender reputation score. For example, you may add validation to a subscription form on your site to let users know they’ve just entered an invalid address. |
| ConfirmedEmail | create, list, delete |
Manage email addresses confirmed for sandbox-domain sending. These methods are available only with a main account API key. |
| Template | set, get, list, delete |
You have the option of storing a template on the UniOne side and then using it by id, which is highly recommended for sending repetitive messages or high volume traffic. UniOne supports two different template engines. |
| Webhook | set, get, list, delete |
UniOne lets you recieve notifications about email status changes or spam blocks. You provide an URL and every time a watched event occurs we will notify you by submitting a request with JSON data to this URL. |
| Suppression | set, get, list, delete |
Methods for managing suppression lists. An email can be added to suppression list because of unsubscription, bounce or complain. |
| Domain | getDnsRecords, validateVerificationRecord, validateDkim, list, getSandboxDomain, delete |
You can use these methods to setup sender domains and check their statuses. They might be useful in case you want to automate adding domains for your own clients. |
| EventDump | create, get, list, delete |
These methods are used to obtain a list of events for a given period of time. The data is stored for up to 45 days, depending on your tariff, i. e. you may fetch all event data starting from 00:00:00 31 days ago and up to current time. For custom tarifs with big sending volumes event dumps are available only for the last 7 days. |
| Tag | list, delete |
This family of methods is used to manage user-defined tags. Tags are used to categorize letters based on your own criteria, e. g. to create a distinction between marketing and transactional letters or group together all notifications for a particular order type. UniOne allows you to assign tags to all outgoing emails, and the tags you’ve set may then be used to filter and analyze delivery statistics. |
| Project | create, update, list, delete |
Projects allow you to group several independent domains, webhooks, templates, and unsubscription lists together. It's very useful if you have your own clients and manage their data from a single UniOne account. Each project receives its own API key, and all API calls with that API key only deal with the data of that project. This way, you can assign a separate project for each client and be sure that they won't conflict; e.g. unsubscribing from one project won't affect another. |
| System | ping, info |
System operations. |
| Obsolete | unsubscribedSet, unsubscribedCheck, unsubscribedList |
Obsolete methods dealing with unsubscribes. We recommend using the more flexible suppression methods instead. Note that you can't resubscribe an unsubscribed recipient using WebAPI, but you can send him an email with subscription link using email/subscribe method. |
API host
<?php use UniOne\UniOne; use UniOne\Client\Configuration; $client = UniOne::client('YOUR_API_KEY', 'eu1.unione.io'); $client = UniOne::client('YOUR_API_KEY', Configuration::HOST_US1); $client = UniOne::client('YOUR_API_KEY', 'https://api.unione.io/en/transactional/api/v1');
HTTP client
<?php use UniOne\UniOne; use UniOne\Client\Transport\Psr18Transport; $factory = new GuzzleHttp\Psr7\HttpFactory(); $client = UniOne::client( 'YOUR_API_KEY', transport: new Psr18Transport(new GuzzleHttp\Client(), $factory, $factory), );
Models
Requests and responses are plain PHP classes. Build a request with named arguments, toArray() gives the JSON body, fromArray() builds an object from decoded JSON. Enum fields hold an enum case or, for a value this SDK version does not know yet, the raw value (EventName|string): such a value is kept when reading a response and can be sent too. Empty objects and maps are sent as {}.
Additional fields
Some objects take keys beyond their named fields, e.g. any header in HeadersObject. They go to the extra map (name => value): fromArray() puts every key that is not a named field there, toArray() sends them next to the named fields (a named field wins over the same key in extra).
<?php use UniOne\Common\Model\HeadersObject; $headers = new HeadersObject( extra: ['X-MyHeader' => 'some value', 'CC' => 'copy@example.com', 'List-Unsubscribe' => '<mailto:unsubscribe@example.com>'], ); var_dump($headers->extra);
Error handling
Every method throws UniOne\Client\ApiException: an error of the API, a network error or a timeout, a broken response, and a mistake found before sending (an empty API key, a missing or empty message.recipients or email of a recipient: no request is sent). The message names the method and, for an API error, the API error code, the HTTP status and how to fix it: email/send: Unknown recipient (API error 204, HTTP 400). Fix: .... getKind() tells what failed (ApiException::KIND_API, KIND_NETWORK, KIND_TIMEOUT, KIND_INVALID_RESPONSE, KIND_INVALID_REQUEST), isRetryable() whether a repeat may succeed (network, timeout, HTTP 429 and 5xx); getErrorCode(), getHttpStatus(), getMethod(), getHint() and getResponseBody() give the details, getApiMessage() the API's own message text in full and undecorated (null without an API response).
Help
- Integration guides
- AI coding agents: llms.txt, the MCP server, skills:
npx skills add https://github.com/unione-repo/unione-skills.git - Support · Issues
- Contributing: AGENTS.md — the SDK is generated from the OpenAPI spec.