# 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](/login), 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**

```bash
# .env
AVELTO_API_KEY=av_test_...
```

**PHP**

```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
<?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.avelto.dev/v1/emails', [
            'from' => 'you@sandbox.avelto.dev',
            'to' => 'delivered@sandbox.avelto.dev',
            '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**

```bash
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](/docs/send-email).

## 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**

```php
Route::get('/emails/{id}', function (string $id) {
    $email = Http::withToken(config('services.avelto.key'))
        ->get("https://api.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**

```bash
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
<?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.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.avelto.dev/v1/domains/{$domain['id']}")
            ->json('status');
    }

    $this->info($status); // "verified" or "failed"
});
```

**shell**

```bash
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 `hello@mail.acme.com`. Nothing else changes.

## Next

- [Send email](/docs/send-email): every field, attachments, tags, scheduling and idempotency.
- [Webhooks](/docs/webhooks): get events pushed to your app.
- [Test mode](/docs/test-mode): test keys, the sandbox sender and the simulator addresses.

---

Rendered page: https://avelto.dev/docs/quickstart/laravel
