PHP code example of recado / recado-php

1. Go to this page and download the library: Download recado/recado-php library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

recado / recado-php example snippets


'mailers' => [
    // ...existing mailers...
    'recado' => [
        'transport' => 'recado',
    ],
],

use App\Mail\WelcomeMail;
use Illuminate\Support\Facades\Mail;

// A Mailable through the transport (MAIL_MAILER=recado)
Mail::to('[email protected]')->send(new WelcomeMail($user));

// A plain message
Mail::raw('Hello world', fn ($m) => $m->to('[email protected]')->subject('Hi'));

use Recado\Sdk\Laravel\Facades\Recado;

Recado::send()->email([
    'to' => '[email protected]',
    'template' => 'welcome',           // template slug from Recado
    'variables' => ['first_name' => 'Jane'],
]);

// ...or fully inline content
Recado::send()->email([
    'to' => '[email protected]',
    'subject' => 'Welcome aboard',
    'body' => '<p>Hi {{ contact.first_name }}!</p>',
    'text' => 'Hi!',
]);

use Recado\Sdk\RecadoClient;

$client = new RecadoClient(
    baseUrl: 'https://app.example.com/api/v1',
    token: 'your-project-api-key',
);

// Send a single transactional email (inline content)
$sent = $client->send()->email([
    'to' => '[email protected]',
    'subject' => 'Welcome aboard',
    'body' => '<p>Hi {{ contact.first_name }}!</p>',
    'text' => 'Hi!',
    'variables' => ['plan' => 'pro'],
]);

echo $sent->id;      // "11111111-2222-..."
echo $sent->status;  // "queued"

// Or send using a stored template by slug
$client->send()->email([
    'to' => '[email protected]',
    'template' => 'welcome',
    'variables' => ['first_name' => 'Jane'],
]);

// Attachments: max 10 files, 10 MB decoded per file and per send total
// (over-total → 422 with code `attachments_too_large`); executable filename
// extensions are rejected. Single sends only — /send/batch rejects them.
$client->send()->email([
    'to' => '[email protected]',
    'template' => 'invoice',
    'attachments' => [
        [
            'filename' => 'invoice.pdf',
            'content_type' => 'application/pdf',
            'content' => base64_encode($pdfBytes), // standard base64, no data: prefix
        ],
    ],
]);

$result = $client->send()->batch([
    ['to' => '[email protected]', 'template' => 'welcome'],
    ['to' => '[email protected]', 'subject' => 'Hi', 'body' => '<p>Hello</p>'],
]);

echo $result->queued; // 2
echo $result->failed; // 0

foreach ($result->messages as $item) {
    // $item->index, $item->status (queued|suppressed|failed), $item->id, $item->code, $item->error
}

$result = $client->notifications()->send([
    'to' => '[email protected]',
    'title' => 'Your order shipped',
    'body' => 'Tracking #1234 is on its way.',
    'channels' => ['in_app', 'push'], // optional; defaults to ['in_app']
    'action_url' => 'https://app.example.com/orders/1234',
    'variables' => ['first_name' => 'Jane'],
]);

if (! $result->anyQueued()) {
    // Nothing could be delivered — inspect the per-channel outcomes below.
}

foreach ($result->messages as $channel) {
    // $channel->channel, $channel->id, $channel->status, $channel->errorCode
}

$push = $result->channel('push');
if ($push && ! $push->queued()) {
    echo $push->errorCode; // e.g. push_provider_not_configured
}

$result = $client->notifications()->send([
    'to' => '[email protected]',
    'template' => 'order-shipped',
    'variables' => ['order_id' => 'ord_8842'],
    'action_url' => 'https://app.example.com/orders/1234', // optional override
]);

$result = $client->notifications()->batch([
    ['to' => '[email protected]', 'title' => 'Shipped', 'body' => 'On its way.'],
    [
        'to' => '[email protected]',
        'title' => 'Shipped',
        'body' => 'On its way.',
        'channels' => ['in_app', 'push'],
    ],
], idempotencyKey: 'orders-2026-08-15'); // optional

echo $result->queued; // channel dispatches accepted
echo $result->failed; // channel dispatches that could not be queued

foreach ($result->messages as $item) {
    // $item->index, $item->to, $item->anyQueued()
    $push = $item->channel('push');
    if ($push && ! $push->queued()) {
        echo $push->errorCode; // e.g. upgrade_

$result = $client->push()->register('[email protected]', 'fcm-device-token', 'android');
echo $result->registered; // true
echo $result->devices;    // number of push devices now on the contact

$removed = $client->push()->remove('[email protected]', 'fcm-device-token');
echo $removed->removed;    // true, or false if the contact had no such token

$client->send()->email(
    ['to' => '[email protected]', 'template' => 'welcome'],
    idempotencyKey: 'order-1234-welcome',
);

$client->send()->batch($messages, idempotencyKey: 'nightly-digest-2026-06-12');

// Track an event
$client->send()->track('order.placed', '[email protected]', ['total' => 4200]);

// ...and set contact fields on the contact the event upserts (4th argument,
// optional): `first_name`, `last_name`, `name` and `locale`. Use `name` when
// you only have the full name — the API splits it on the first whitespace, and
// explicit `first_name`/`last_name` always win. Every field is set on create,
// updated when provided, and never cleared when omitted.
$client->send()->track('signed-up', '[email protected]', [], [
    'first_name' => 'Jane',
    'last_name' => 'Doe',
    'locale' => 'es-MX',
]);
$client->send()->track('signed-up', '[email protected]', [], ['name' => 'Ada Lovelace']);

// The same array also takes `lists` (ids of your lists, max 50) and `tags`
// (names, max 25, created on first use). The API attaches them BEFORE it
// records the occurrence, so an event-triggered automation already sees them
// (a `has_tag` condition can match a tag sent with this very call). Both are
// idempotent and neither changes the contact's subscription status; an unknown
// list id throws a `ValidationException` with the code `list_not_found`.
$client->send()->track('signed-up', '[email protected]', [], [
    'name' => 'Ada Lovelace',
    'lists' => [1],
    'tags' => ['beta'],
]);

// The positional arguments always win: an `email`/`event`/`data` key in the
// contact array is ignored, never a way to redirect the call.

// Subscribe a contact (double opt-in aware)
$client->send()->subscribe([
    'email' => '[email protected]',
    'first_name' => 'Jane',
    'lists' => [7],
    'tags' => ['newsletter'],
]);

// Contacts
$page = $client->contacts()->list(['status' => 'subscribed', 'per_page' => 50]);
$contact = $client->contacts()->get('[email protected]');
$client->contacts()->update('[email protected]', ['first_name' => 'Janet']);

// Bulk attribute upsert: up to 500 contacts per request, own 30/min limiter.
// Update-only (unknown emails come back as `skipped_not_found`, never created)
// and consent-safe (status, subscribed_at/unsubscribed_at and list memberships
// are never written). Attributes MERGE, so absent keys survive.
$result = $client->contacts()->batchUpdate([
    [
        'email' => '[email protected]',
        'attributes' => ['plan' => 'pro', 'last_active_at' => '2026-09-10'],
        'tags_add' => ['power-user'],
        'tags_remove' => ['trial'],
    ],
], idempotencyKey: 'attribute-refresh-2026-09-10');
// $result['results'][0]['status'] === 'updated' | 'skipped_not_found' | 'invalid_attributes'

$client->contacts()->tags('[email protected]', add: ['vip'], remove: ['trial']);
$client->contacts()->cancelAutomationRuns('[email protected]', automation: 12);
$client->contacts()->delete('[email protected]'); // GDPR erase

// Lists
$lists = $client->lists()->list();
$list = $client->lists()->create('Newsletter', 'Weekly digest');
$client->lists()->attachContact($list->id, '[email protected]');
$client->lists()->detachContact($list->id, '[email protected]');
$client->lists()->update($list->id, ['name' => 'Weekly digest']); // membership untouched
$client->lists()->delete($list->id);                              // contacts survive

// Tags (flat array) — full CRUD, including the preference-center fields
$tags = $client->tags()->list();
// create() is create-or-FIND: an existing name (matched case-insensitively)
// comes back unchanged, so it never repaints a tag someone already curated.
$tag = $client->tags()->create('vip', [
    'color' => '#16a34a',
    'is_public' => true,                  // shows as an opt-in checkbox on the
    'public_label' => 'Insider news',     // preference center, labelled by
    'public_description' => 'Occasional early access.',
]);
$client->tags()->update($tag->id, ['public_label' => 'Insiders']);
$client->tags()->delete($tag->id); // detached from every contact; contacts survive

// Bulk contact imports — the only surface that creates contacts in bulk WITH
// their consent state. Asynchronous: poll until the run is finished.
$import = $client->imports()->create([
    ['email' => '[email protected]', 'first_name' => 'Ada', 'tags' => ['vip']],
    ['email' => '[email protected]', 'status' => 'unsubscribed'],
], ['lists' => [12], 'tags' => ['migration-2026']]);

$import = $client->imports()->get($import->id);
$import->isFinished(); // status is completed or failed
$import->invalidStatusRows; // a warning: the row imported, its status did not parse

// Templates
$templates = $client->templates()->list();
$template = $client->templates()->create([
    'name' => 'Welcome',
    'slug' => 'welcome',
    'subject' => 'Welcome!',
    'body_html' => '<p>Hi</p>',
]);
$client->templates()->putVariant('welcome', 'es', [
    'subject' => '¡Bienvenido!',
    'body_html' => '<p>Hola</p>',
]);

// Notification templates (the in-app/push sibling — addressed by slug, which is
// what notifications()->send() accepts as `template`)
$client->notificationTemplates()->create([
    'name' => 'Order shipped',
    'slug' => 'order-shipped',
    'title' => 'On its way, {{ contact.first_name }}',
    'body' => 'Your order left the warehouse.',
    'action_url' => 'myapp://orders/42', // deep links are allowed, scripts never
]);
// A variant is a FULL replace: omitting action_url RESETS it for that locale.
$client->notificationTemplates()->putVariant('order-shipped', 'es-MX', [
    'title' => 'En camino',
    'body' => 'Tu pedido salió del almacén.',
]);
$client->notificationTemplates()->delete('order-shipped');

// Messages
$messages = $client->messages()->list(['status' => 'delivered']);
$message = $client->messages()->get('11111111-2222-...');
foreach ($message->events as $event) {
    // $event->type, $event->payload, $event->occurredAt
}
// Resend: queues a BRAND-NEW message with the original's recipient and rendered
// content. Suppression, quota and warm-up all re-run, so the copy can still be
// refused (`message_not_resendable`, `recipient_suppressed`, `quota_exceeded`).
$copy = $client->messages()->resend($message->uuid);

// Campaigns (full lifecycle — see "Campaigns" below)
$campaigns = $client->campaigns()->list(['status' => 'sent', 'per_page' => 50]);
$campaign = $client->campaigns()->get(7);
// $campaign->stats is a populated CampaignStats on get() (and on
// list(['

$campaign = $client->campaigns()->create([
    'name' => 'July product update',
    'subject' => 'What shipped in July',
    'editor' => 'markdown', // blocks (default) | html | markdown — immutable
    'content' => ['source' => '# Hi {{ contact.first_name }}'],
    'lists' => [3],
    'segments' => [7],
]);

$client->campaigns()->update($campaign->id, ['subject' => 'What actually shipped']);

// Look before you leap: how many people, what would fail, what it looks like.
$client->campaigns()->recipientCount(lists: [3], segments: [7]); // int
$readiness = $client->campaigns()->readiness($campaign->id);
foreach ($readiness->failures() as $check) {
    echo $check->key.': '.$check->code.PHP_EOL; // e.g. quota: quota_exceeded
}
$preview = $client->campaigns()->preview($campaign->id, contactEmail: '[email protected]');
$client->campaigns()->testSend($campaign->id, ['[email protected]']);

$campaign = $client->campaigns()->create([
    'name' => 'July product update',
    'editor' => 'markdown',
    'subject' => 'What shipped in July',       // the base every variant inherits
    'content' => ['source' => '# Hi {{ contact.first_name }}'],
    'lists' => [3],
    'ab_test' => [
        'enabled' => true,
        'test_fraction' => 0.2,        // 0.1..0.5 of the audience (default 0.2)
        'winner_metric' => 'opens',    // opens | clicks       (default opens)
        'test_duration_minutes' => 240, // 30..2880            (default 240)
    ],
    'variants' => [
        ['subject' => 'What shipped in July'],   // inherits the campaign body
        ['subject' => 'July: 11 new things', 'content' => ['source' => '# Eleven']],
    ],
]);

$campaign->abTest->variants[0]->label; // 'A' — labels are server-assigned

if ($campaign->abTest?->locked) {
    // ab_test_state is testing/deciding/finished — a write touching the
    // variants now is a 422 with code ab_test_locked.
}

$campaign = $client->campaigns()->create([
    'name' => 'July product update',
    'editor' => 'markdown',
    'subject' => 'What shipped in July',
    'content' => ['source' => '# Hi {{ contact.first_name }}'],
    'lists' => [3],
    'locale_variants' => [
        ['locale' => 'es', 'subject' => 'Lo que lanzamos en julio', 'content' => ['source' => '# Hola']],
        ['locale' => 'fr', 'subject' => 'Nouveautes de juillet'],  // French subject, English body
    ],
    'variants' => [
        [
            'subject' => 'What shipped in July',
            'locale_variants' => [['locale' => 'es', 'subject' => 'Lo que lanzamos en julio']],
        ],
        [
            'subject' => 'July: 11 new things',
            'locale_variants' => [['locale' => 'es', 'subject' => 'Julio: 11 novedades']],
        ],
    ],
]);

$campaign->localeVariants[0]->locale;                    // 'es'
$campaign->abTest->variants[1]->localeVariants[0]->subject; // 'Julio: 11 novedades'

$client->campaigns()->preview($campaign->id, locale: 'es');
$client->campaigns()->testSend($campaign->id, ['[email protected]'], variant: 6, locale: 'es');

use Recado\Sdk\Exception\CampaignSendNotConfirmedException;

$client->campaigns()->send($campaign->id, confirm: true); // sends

$client->campaigns()->send($campaign->id); // throws CampaignSendNotConfirmedException

$client->campaigns()->schedule($campaign->id, '2026-07-05T09:00:00+00:00');
$client->campaigns()->unschedule($campaign->id);   // back to draft
$client->campaigns()->cancel($campaign->id);       // scheduled/sending → cancelled
$copy = $client->campaigns()->duplicate($campaign->id, 'Week 38'); // fresh draft
$client->campaigns()->delete($campaign->id);       // draft/cancelled/failed only

// Filters, sorting and embedded stats for a table view.
$page = $client->campaigns()->list([
    'status' => ['sent', 'failed'],
    'search' => 'July',
    'sort' => '-scheduled_at',
    'include' => 'stats',
]);

// Refresh the metrics of rows you already hold (one aggregate query).
$stats = $client->campaigns()->stats([31, 32]); // [31 => CampaignStats, ...]

// Click and A/B reporting on the detail endpoint.
$campaign = $client->campaigns()->get(31, ['include' => 'top_links,variants']);
$campaign->topLinks[0]->url;
$campaign->variants[0]->isWinner;

// Publication flags (drafts only, like every campaign field):
//  - in_archive (default true): listed on the project's public archive page
//    and RSS feed once the campaign has been sent.
//  - premium (default false): goes to paid subscribers only. Turning it ON
//    

// Size the audience per channel BEFORE creating anything. Every sendable
// channel is counted, not only the ones a broadcast selects.
$counts = $client->broadcasts()->recipientCount(lists: [3], segments: [7]);
$counts->for('push');        // 612
$counts->recipientsTotal;    // the SUM: a contact reachable twice is two sends

$broadcast = $client->broadcasts()->create([
    'name' => 'Launch day',                 // internal only
    'title' => 'We launched',
    'body' => 'The new dashboard is live.',
    'action_url' => 'myapp://dashboard',    // deep links allowed, scripts never
    'channels' => ['in_app', 'push'],       // `email` is rejected
    'lists' => [3],
    'segments' => [7],
]);

// Title, body and channels may stay empty while drafting; they are enforced at
// send time, exactly like a campaign's subject and content.
$client->broadcasts()->update($broadcast->id, ['channels' => ['push']]);

// A proof to ONE existing contact of the project (an unknown address could only
// produce a blocked send). It carries no broadcast id, so stats stay clean.
$client->broadcasts()->testSend($broadcast->id, '[email protected]');

// Sending needs the same explicit confirmation a campaign send does: without
// `confirm: true` this throws CampaignSendNotConfirmedException locally and no
// request is made.
$client->broadcasts()->send($broadcast->id, confirm: true);

$client->broadcasts()->schedule($broadcast->id, '2026-12-01T09:00:00Z');
$client->broadcasts()->unschedule($broadcast->id); // back to draft
$client->broadcasts()->cancel($broadcast->id);     // ends `cancelled`, not draft

$client->broadcasts()->get($broadcast->id)->stats?->openRate; // null on a zero denominator

foreach ($client->waitlists()->cursor(['status' => 'open', 'cal public host
    $waitlist->membersCount;     // total signups ever recorded
    $waitlist->stats?->last7Days; // null unless you asked for the full list.
foreach ($client->waitlists()->membersCursor(7) as $member) {
    $member->position;        // 1-based
    $member->referralCode;    // the code in their own share link (not a secret)
    $member->referralsCount;  // credited referrals, never decremented
}

$waitlist = $client->waitlists()->launch(7);

// It claims open → launched atomically, stops the public signup page, tags
// every member contact `early-adopter` through the normal tag path (so
// `tag_added` automations fire), and creates a DRAFT announcement campaign
// targeted at the waitlist's own list:
$waitlist->launchCampaignId;

// Pass false to skip that campaign:
$client->waitlists()->launch(7, createCampaign: false);

$domain = $client->sendingDomains()->add('mail.example.com');

foreach ($domain->records as $record) {
    // Shaped for a DNS panel: `host` relative to the zone, `fqdn` alongside,
    // an MX `priority` split into its own field, a concrete `ttl` suggestion.
    $record->type; $record->host; $record->fqdn; $record->value; $record->priority;
}

// `dmarc` is a RECOMMENDATION, never a 

foreach ($domain->missingRecords() as $record) {
    // Still on your side to publish.
}

$client->sendingDomains()->list();     // every identity, alphabetically
$client->sendingDomains()->get(12);
$client->sendingDomains()->delete(12); // also deletes the provider identity

$domain = $client->customDomains()->add('news.acme.com');
$domain->record->type;   // CNAME
$domain->record->value;  // the platform subdomain to point at

$domain = $client->customDomains()->check($domain->id);
$domain->isVerified();
$domain->tlsPending;     // DNS ok, certificate still on its way
$domain->tlsError;       // rate_limited | rejected | api_error | certificate_failed | timeout | exhausted

$client->customDomains()->current();  // ?CustomDomain
$client->customDomains()->delete($domain->id);

$estimate = $client->verification()->estimate(listId: 12);

$estimate->contactsInScope;   // 1840
$estimate->addresses;         // 1204 — what you would actually pay for
$estimate->creditsRemaining;  // null when the provider reports no balance
$estimate->sufficientCredits;

$run = $client->verification()->run($estimate);

while (! $run->isFinished()) {
    sleep(5);
    $run = $client->verification()->get($run->id);
}

$run->updated;  // addresses the provider answered for
$run->failed;   // lookups that could not be made — a provider outage never
                // rewrites a stored verdict

use Recado\Sdk\Exception\VerificationEstimateMismatchException;

try {
    $run = $client->verification()->run($estimate);
} catch (VerificationEstimateMismatchException $e) {
    $fresh = $e->currentEstimate();          // same scope, current numbers
    $run = $client->verification()->run($fresh);
}

use Recado\Sdk\Exception\RecadoException;

try {
    $client->sendingDomains()->list();
} catch (RecadoException $e) {
    if ($e->isNotAvailableInSandbox()) {
        // This credential is a sandbox key. Use the production one.
    }
}

foreach ($client->contacts()->cursor(['status' => 'subscribed']) as $contact) {
    echo $contact->email.PHP_EOL;
}

// Available cursors (each yields the same DTOs as the matching list()):
$client->contacts()->cursor($query);           // Contact
$client->messages()->cursor($query);           // Message
$client->campaigns()->cursor($query);          // Campaign
$client->segments()->cursor($query);           // Segment
$client->events()->cursor($query);             // EventOccurrence
$client->events()->forContactCursor($email);   // EventOccurrence
$client->lists()->cursor($query);              // ContactList
$client->lists()->contactsCursor($id, $query); // Contact
$client->templates()->cursor($query);          // Template

use Illuminate\Support\LazyCollection;

LazyCollection::make($client->contacts()->cursor())
    ->filter(fn ($contact) => $contact->status === 'subscribed')
    ->each(fn ($contact) => /* ... */);

$client = new RecadoClient(
    baseUrl: 'https://app.example.com/api/v1',
    token: 'your-project-api-key',
    httpClient: null,
    options: [
        'retries' => 2,            // max retry attempts
        'retry_base_delay' => 200, // ms, exponential backoff base
        'retry_max_delay' => 5000, // ms, backoff cap
        'retry_on_status' => range(500, 599), // statuses to retry (429 always retried)
        'timeout' => 30,           // Guzzle request timeout (seconds)
        'connect_timeout' => 10,   // Guzzle connect timeout (seconds)
    ],
);

use Recado\Sdk\Exception\ValidationException;
use Recado\Sdk\Exception\RateLimitException;
use Recado\Sdk\Exception\RecadoException;

try {
    $client->send()->email(['to' => '[email protected]', 'template' => 'welcome']);
} catch (ValidationException $e) {
    if ($e->getErrorCode() === 'recipient_suppressed') {
        // address is on the suppression list — skip it
    }
    $fieldErrors = $e->errors(); // ['to' => ['The to field is 

// 1. Send through the code under test (sandbox token configured).
$client->send()->email(['to' => '[email protected]', 'template' => 'welcome']);

// 2. The sandbox captures every send — read it back like an inbox.
$message = $client->messages()->list(['status' => 'queued'])->data[0];

// 3. Drive the pipeline: simulate provider/engagement events on that message.
$client->sandbox()->simulate($message->uuid, SandboxResource::EVENT_DELIVERED);
$client->sandbox()->simulate($message->uuid, SandboxResource::EVENT_OPEN);
$client->sandbox()->simulate($message->uuid, SandboxResource::EVENT_CLICK, linkIndex: 0);

// 4. Assert on the resulting state.
$refreshed = $client->messages()->get($message->uuid);
// $refreshed->events now contains delivered / opened / clicked

use Recado\Sdk\Resources\SandboxResource;

// Available events (plain strings work too):
SandboxResource::EVENT_DELIVERED;   // 'delivered'
SandboxResource::EVENT_HARD_BOUNCE; // 'hard_bounce'
SandboxResource::EVENT_SOFT_BOUNCE; // 'soft_bounce'
SandboxResource::EVENT_COMPLAINT;   // 'complaint'
SandboxResource::EVENT_OPEN;        // 'open'
SandboxResource::EVENT_CLICK;       // 'click' — pass linkIndex or url
SandboxResource::EVENT_READ;        // 'read'

use Recado\Sdk\RecadoClient;

public function __construct(private RecadoClient $recado) {}

// ...
$this->recado->send()->email([...]);

'mailers' => [
    // ...
    'recado' => [
        'transport' => 'recado',
    ],
],

// Plain message
Mail::raw('Hello world', fn ($m) => $m->to('[email protected]')->subject('Hi'));

// A Mailable
Mail::to('[email protected]')->send(new WelcomeMail($user));

use Recado\Sdk\Laravel\Mail\RecadoHeaders;

class WelcomeMail extends Mailable
{
    public function build()
    {
        return $this
            ->subject('Welcome') // ignored when a template header is present
            ->withSymfonyMessage(function ($message) {
                $headers = $message->getHeaders();
                $headers->addTextHeader(RecadoHeaders::TEMPLATE, 'welcome');
                $headers->addTextHeader(
                    RecadoHeaders::VARIABLES,
                    json_encode(['first_name' => 'Jane']),
                );
            });
    }
}

use Recado\Sdk\Laravel\Mail\RecadoMessage;

public function toRecado($notifiable): RecadoMessage
{
    return (new RecadoMessage)
        ->subject('Your order shipped')
        ->html('<p>It is on the way.</p>')
        ->text('It is on the way.');
}

return (new RecadoMessage)
    ->template('order-shipped')
    ->variables(['name' => $notifiable->name]);

use Illuminate\Notifications\Notification;
use Recado\Sdk\Laravel\Mail\RecadoMessage;

class OrderShipped extends Notification
{
    public function via($notifiable): array
    {
        return ['recado'];
    }

    public function toRecado($notifiable): RecadoMessage
    {
        return (new RecadoMessage)
            ->subject('Your order shipped')
            ->html('<p>It is on the way.</p>');
        // Or a stored template:
        // return (new RecadoMessage)->template('order-shipped')->variables(['name' => $notifiable->name]);
    }
}

use Recado\Sdk\Laravel\Facades\Recado;

Recado::contacts()->list();
Recado::send()->email([
    'to' => '[email protected]',
    'subject' => 'Hello',
    'body' => '<p>Hi</p>',
]);
bash
composer 
json
{
    "repositories": [
        { "type": "path", "url": "sdk/php" }
    ],
    "
bash
composer 
bash
composer 
bash
php artisan vendor:publish --tag=recado-sdk-config