Search by

misarai / misarmail-php

mrgulshanyadav

PHP client for MisarMail's transactional and marketing email API: send mail, run campaigns and A/B tests, manage contacts, segments, templates and automations, verify domains and DMARC, validate addresses, track revenue and read analytics — 33 resource groups, retries, typed plan-limit errors, SSE s

v5.0.3 2026-08-18 22:03 UTC

This package is auto-updated.

Last update: 2026-09-18 22:15:35 UTC


README

Send transactional email and run marketing campaigns from PHP — one client, ext-curl and ext-json only.

Packagist php license

33 resource groups · 90 methods · SSE streaming · typed plan-limit errors

MisarMail is one API for both halves of your email: the receipts and password resets your product sends, and the campaigns, segments and automations your marketing team runs on the same contact list and the same verified domains.

PHP 8.1+, with nothing but ext-curl and ext-json underneath. Requests take plain associative arrays and every method returns a decoded array — no DTOs to learn.

Install

Composer

composer require misarai/misarmail-php

composer.json

{ "require": { "misarai/misarmail-php": "^1.0" } }

Authentication

Create a developer key at https://mail.misar.io/developers. It starts with msk_ and is sent as Authorization: Bearer msk_….

Every call is metered against the subscription attached to that key. There is no client-side limit checking — the server decides, and the SDK surfaces its answer. A plan refusal answers 403 with code: "plan_limit_exceeded" and is never retried.

use MisarMail\Client;

$mail = new Client(getenv('MISARMAIL_API_KEY'));

Resources

Every group the client exposes, and every public method on it.

Send

Resource Methods What it covers
$mail->email send Transactional send — cc/bcc/reply-to, tags, metadata, idempotency_key.
$mail->sandbox send, list, delete Test sends captured instead of delivered.

Campaigns and tests

Resource Methods What it covers
$mail->campaigns list, create, get, update, send, delete Marketing campaigns: draft, edit, queue for send.
$mail->abTests list, create, get, setWinner Subject, content, send-time, from-name and preheader splits, and winner selection.

Audience

Resource Methods What it covers
$mail->contacts list, create, get, update, delete, import Subscribers, plus bulk import.
$mail->segments members Dynamic audience segments and their membership.
$mail->landingPages create Hosted landing pages with an email capture form.

Content

Resource Methods What it covers
$mail->templates list, create, get, update, delete, render Reusable templates and server-side variable rendering.
$mail->ai subjectLines AI-generated subject lines.

Automations

Resource Methods What it covers
$mail->automations list, create, get, update, delete, activate Trigger-based workflows — welcome series, drips, re-engagement.

Deliverability and sending infrastructure

Resource Methods What it covers
$mail->domains list, create, get, verify, delete Sending domains and their DNS verification.
$mail->dmarc check, listDomains, addDomain, removeDomain Live SPF/DKIM/DMARC record checks and monitored domains.
$mail->deliverability audit, score Deliverability score, audit and remediation guidance.
$mail->dedicatedIps list, create, update, delete Dedicated sending IPs.
$mail->warmup get IP/domain warm-up progress and today's remaining capacity.
$mail->inbound list, create, get, delete Inbound routing domains, so replies land in the unified inbox.

Mailbox and inbox

Resource Methods What it covers
$mail->emails list, get, update Stored messages in the mailbox.
$mail->emailAccounts list Connected mailbox accounts.

Analytics and attribution

Resource Methods What it covers
$mail->analytics overview Delivery and engagement stats — aggregate, or one campaign.
$mail->track event, purchase Custom events and ecommerce purchases.
$mail->revenue attribution Revenue attributed back to email.
$mail->usage get Metered usage for a period.

Validation

Resource Methods What it covers
$mail->validate email Address validation, and the credit balance behind it.

Plan, billing and credits

Resource Methods What it covers
$mail->plan get, monetization Current plan, quotas and monetization stats.
$mail->billing subscription, checkout Subscription state and checkout.
$mail->subscription get, upsert, cancel Subscription read/write and per-product plan limits.
$mail->wallet get, credit, debit Credit balance, credit and debit.
$mail->creditRates list What each metered action costs in credits.
$mail->teamMembers get Team members on the account.
$mail->monetization tip Newsletter tips.

Developer

Resource Methods What it covers
$mail->keys list, create, get, revoke API keys — create, list, revoke.
$mail->webhooks list, create, get, update, delete, test Webhook endpoints, plus a test delivery.
$mail->streaming generateEmail, campaignSend The two Server-Sent Events endpoints.

Client

Thing Detail
Entry point new MisarMail\Client($apiKey, $timeout = 30, $baseUrl = null) — every resource is a readonly property.
$baseUrl / $apiBase https://api.misar.io/mail/v1 and the derived base for routes outside /v1; both readable on the client.
Results A decoded array. 204/empty comes back as [].
Transport cURL, 10-second connect timeout, $timeout for the rest.
Retried 429, 500, 502, 503, 504 and cURL failures — 3 attempts, 300 ms then 600 ms.
Never retried Plan refusals, and streams.
Errors MisarMail\ApiError, with NetworkError and PlanLimitError extending it.
Webhook verifier Not shipped here — verify HMAC-SHA256(timestamp . "." . rawBody) yourself with hash_hmac() and hash_equals(). (Go, Python, Ruby and Dart ship one.)
Escape hatch $mail->request($method, $path, $data, $baseOverride) is public.

Quick start

<?php
require 'vendor/autoload.php';

use MisarMail\Client;

$mail = new Client(getenv('MISARMAIL_API_KEY'));

$sent = $mail->email->send([
    'from'    => ['email' => 'you@yourdomain.com', 'name' => 'Your App'],
    'to'      => [['email' => 'someone@example.com']],
    'subject' => 'Hello',
    'html'    => '<p>Hi there</p>',
]);

echo $sent['message_id'];

Primary functions

Send a transactional email

from is a single address array and to is a list of them. Pass an idempotency_key and a retry can never send twice — the response comes back with idempotent: true the second time.

$res = $mail->email->send([
    'from'            => ['email' => 'receipts@yourdomain.com', 'name' => 'Acme'],
    'to'              => [['email' => 'customer@example.com']],
    'reply_to'        => ['email' => 'support@yourdomain.com'],
    'subject'         => 'Your receipt',
    'html'            => '<p>Thanks for your order.</p>',
    'text'            => 'Thanks for your order.',
    'tags'            => ['receipt'],
    'metadata'        => ['order_id' => 'ord-1041'],
    'idempotency_key' => 'ord-1041-receipt',
]);

$res['message_id'];   // "msg-…"

List and create contacts

Responses are enveloped. list() returns ['success', 'data', 'pagination'] and takes page/limit as named integers, not an options array; create() returns ['success', 'data'] with the contact under data.

$page = $mail->contacts->list(page: 1, limit: 50);
echo count($page['data']), ' of ', $page['pagination']['total'];

$created = $mail->contacts->create([
    'email'        => 'new@example.com',
    'firstName'    => 'Ada',
    'lastName'     => 'Lovelace',
    'tags'         => ['beta'],
    'customFields' => ['plan' => 'pro'],
]);

echo $created['data']['id'];

get() and delete() take the contact id, which the route reads from the query string rather than a path segment. update() is different again: it identifies the contact by email address, not by id.

$mail->contacts->update('ada@example.com', ['status' => 'unsubscribed']);

Bulk import contacts

Counts come back under summary, and errors is a separate list of messages.

$imported = $mail->contacts->import([
    'contacts' => [
        ['email' => 'a@example.com', 'firstName' => 'A'],
        ['email' => 'b@example.com', 'firstName' => 'B'],
    ],
    'updateExisting' => true,
]);

$imported['summary'];   // ['imported' => …, 'updated' => …, 'skipped' => …, 'errors' => …]
$imported['errors'];    // list of strings

Create and send a campaign

Campaigns take fromName and fromEmail as separate fields — there is no from array here, unlike email->send(). send() queues the campaign and reports it as scheduled.

$campaign = $mail->campaigns->create([
    'name'      => 'March launch',
    'subject'   => 'We just shipped',
    'fromName'  => 'Ada at Acme',
    'fromEmail' => 'hello@yourdomain.com',
    'replyTo'   => 'support@yourdomain.com',
    'bodyHtml'  => '<h1>It is live</h1>',
    'segmentId' => 'seg-123',
]);

$queued = $mail->campaigns->send($campaign['data']['id']);
echo $queued['campaignId'], ' ', $queued['status'];   // … scheduled

Only draft, scheduled or paused campaigns can be updated, and only draft campaigns can be deleted.

Validate an address

Each call spends a credit, and the response tells you what is left.

$check = $mail->validate->email('someone@example.com');

$check['data']['is_valid'];             // bool
$check['data']['score'];                // 0–1 confidence
$check['data']['checks'];               // ['syntax' => …, 'mx' => …, 'smtp' => …]
$check['data']['flags']['disposable'];  // bool
$check['credits']['balance_after'];     // credits remaining

Render a template

$rendered = $mail->templates->render([
    'template_id' => 'tpl-123',
    'variables'   => ['name' => 'Ada', 'plan' => 'Pro'],
]);

$rendered['data']['subject'];   // "Welcome, Ada"
$rendered['data']['html'];

Track events and revenue

The event name field is event_name, and purchase totals are integer cents in total_cents.

$mail->track->event([
    'email'      => 'customer@example.com',
    'event_name' => 'viewed_pricing',
    'event_data' => ['plan' => 'pro'],
]);

$purchase = $mail->track->purchase([
    'email'       => 'customer@example.com',
    'order_id'    => 'ord-1041',
    'total_cents' => 9900,
    'currency'    => 'USD',
    'items'       => [['name' => 'Pro annual', 'quantity' => 1, 'price_cents' => 9900]],
]);

$purchase['attribution'];   // which campaign or automation earned it

Read analytics and manage keys

Without campaignId you get aggregate usage and totals for the period; with one you get that campaign's stats and rates. keys->list() returns the keys under keys, not data, and create() returns the raw key exactly once.

$overall = $mail->analytics->overview(['startDate' => '2026-04-01', 'endDate' => '2026-04-30']);
$one     = $mail->analytics->overview(['campaignId' => $campaignId]);

$keys = $mail->keys->list();
count($keys['keys']);

$fresh = $mail->keys->create(['name' => 'CI', 'scopes' => ['send', 'read']]);
echo $fresh['key'];   // shown once and never again

Errors

Three classes, all in the MisarMail namespace:

Class When
ApiError Any non-2xx API response. Carries $status. Extends RuntimeException.
NetworkError cURL never got an answer, or every retry was spent. $status is 0.
PlanLimitError The subscription behind the key does not cover the call.

NetworkError and PlanLimitError both extend ApiError, so catch (MisarMail\ApiError $e) catches everything the SDK throws.

Plan limits

Both a spent allowance and a feature that is not on the plan answer 403, carrying code: "plan_limit_exceeded". The SDK keys on that code rather than the status, which is why a refusal is typed correctly even though 403 is otherwise an authorization failure. It throws PlanLimitError and does not retry — retrying cannot help until the allowance resets or the plan changes. Read $upgradeUrl to send the user somewhere useful.

try {
    $mail->campaigns->create([
        'name'      => 'Blast',
        'subject'   => 'We just shipped',
        'fromName'  => 'Your Name',
        'fromEmail' => 'you@yourdomain.com',
    ]);
} catch (MisarMail\PlanLimitError $e) {
    fwrite(STDERR, "{$e->feature} exhausted on {$e->plan}: {$e->upgradeUrl}\n");
    // $e->retryAfter is seconds until the allowance resets, when the API says so
}

$mail->plan->get() returns plan, sending (the per-day and per-month email caps), usage — an array with one entry per metered feature, each carrying used, limit and remaining — and upgrade, which is null until a quota is tight. A null limit means unlimited, and remaining is null alongside it rather than 0. Read it before an expensive call rather than discovering the ceiling through a refusal.

The key needs the read or subscription scope.

$plan = $mail->plan->get();
print_r($plan['sending']);
print_r($plan['usage']);

Streaming

Two endpoints stream Server-Sent Events. Both sit outside /v1, which the SDK handles for you:

Method Route
streaming->generateEmail() POST /api/ai/generate-email/stream
streaming->campaignSend() GET /api/campaigns/{id}/send-stream

PHP has no async iterator here, so both take a callback and block until the stream ends. Return false from the callback to stop early. Frames are unnamed (data: {…}, with no event: line) and the stream ends with data: [DONE], which the SDK consumes rather than handing on. Each StreamEvent carries $event (normally null), $data (the decoded array, or null when the payload was not JSON) and $raw. A stream is never retried: replaying one that failed mid-flight would duplicate whatever you had already read.

$mail->streaming->generateEmail(['prompt' => 'a launch email'],
    function (MisarMail\StreamEvent $e) {
        echo $e->data['delta'] ?? '';
    });

$mail->streaming->campaignSend($campaignId, function (MisarMail\StreamEvent $e) {
    echo $e->raw, "\n";
});

Links

MIT © Misar AI