Send email from Laravel

Five steps: nothing to install, create a test key, send to the sandbox from a route, read the log, then verify a domain from an Artisan command so you can send to anyone.

1. Install

No SDK needed. The API is plain JSON over HTTPS, and Laravel's Http facade already handles bearer tokens, JSON bodies and error checks.

2. Create an API key

Sign in, open API keys in the dashboard and create a test key. It starts with av_test_. Add it to .env and expose it through config/services.php, the usual place for third-party credentials:

.env
# .env
AVELTO_API_KEY=av_test_...
PHP
// config/services.php
'avelto' => [
    'key' => env('AVELTO_API_KEY'),
],
Sandbox rules

Test keys never deliver anything; they run the pipeline and record events. The sandbox sender you@sandbox.avelto.dev only delivers to your account's verified owner email and to the simulator addresses delivered@, bounced@ and complained@sandbox.avelto.dev. Anything else is refused with 403 sandbox_recipient_not_allowed. To send to anyone, verify a domain (step 5).

3. Send your first email

A POST route in routes/api.php. On Laravel 11 and later that file only exists once you have run php artisan install:api, and its routes are served under /api. A rejected send comes back as a non-2xx status with { "error": { "code", "message" } }; the route passes that through unchanged.

PHP
<?php
// routes/api.php

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route;

Route::post('/send', function () {
    $response = Http::withToken(config('services.avelto.key'))
        ->post('https://api-staging.avelto.dev/v1/emails', [
            'from' => '[email protected]',
            'to' => '[email protected]',
            'subject' => 'Hello from Avelto',
            'text' => 'It works.',
        ]);

    if ($response->failed()) {
        // { "error": { "code": "...", "message": "..." } }
        return response()->json($response->json(), $response->status());
    }

    return response()->json(['id' => $response->json('id')], 201);
});

Run php artisan serve and call the route:

shell
curl -X POST http://localhost:8000/api/send

The API answers 201 Created with the email id, and the route returns the same:

HTTP
HTTP/1.1 201 Created
Content-Type: application/json

{ "id": "9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10" }
Retrying safely

Send an Idempotency-Key header (any unique string, such as your order id) with every POST /v1/emails. If the request times out or comes back 429, 502, 503 or 504, wait a moment and send it again unchanged with the same key: the API returns the original email id instead of sending twice. See Idempotency.

4. Check the log

A GET route fetches the email by id. status moves from queued to sent to delivered, and events records each step: email.queued, email.sent, email.delivered.

PHP
Route::get('/emails/{id}', function (string $id) {
    $email = Http::withToken(config('services.avelto.key'))
        ->get("https://api-staging.avelto.dev/v1/emails/{$id}")
        ->throw()
        ->json();

    return response()->json([
        'status' => $email['status'], // "queued", then "sent", then "delivered"
        'events' => array_column($email['events'], 'type'),
    ]);
});
shell
curl http://localhost:8000/api/emails/9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10

5. Verify a domain

Adding a domain is a one-off task, so it fits an Artisan command. Publish the DNS records it prints (three DKIM CNAMEs, an SPF TXT and a DMARC TXT) at your DNS provider; the command polls GET /v1/domains/:id, which re-checks DNS on every call, until status is verified. Use a subdomain such as mail.acme.com.

PHP
<?php
// routes/console.php

use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Http;

Artisan::command('avelto:verify {name}', function (string $name) {
    $http = Http::withToken(config('services.avelto.key'));

    $domain = $http->post('https://api-staging.avelto.dev/v1/domains', ['name' => $name])
        ->throw()
        ->json();

    foreach ($domain['dns_records'] as $r) {
        $this->line("{$r['type']}\t{$r['name']}\t{$r['value']}\t({$r['purpose']})");
    }

    // Publish the records, then poll. GET re-checks DNS on every call.
    $status = $domain['status'];
    while ($status === 'pending') {
        sleep(30);
        $status = $http
            ->get("https://api-staging.avelto.dev/v1/domains/{$domain['id']}")
            ->json('status');
    }

    $this->info($status); // "verified" or "failed"
});
shell
php artisan avelto:verify mail.acme.com

Once the domain is verified, switch AVELTO_API_KEY in .env to a live key (av_live_) and change from in the route to an address on it, such as [email protected]. Nothing else changes.

Next

  • Send email: every field, attachments, tags, scheduling and idempotency.
  • Webhooks: get events pushed to your app.
  • Test mode: test keys, the sandbox sender and the simulator addresses.