# Send email with PHP

> Send email from PHP with Email Fast: a complete cURL example against the REST API with a sandbox key, an idempotency key, and honest error handling.

Canonical: https://emailfast.dev/send-email-with/php

PHP needs nothing beyond the bundled cURL extension — the Email Fast API is one JSON
POST with a bearer token. Unlike `mail()`, you get a message id back, with a full
event timeline behind it. If you want a typed client instead, the
[PHP SDK quickstart](/docs/quickstart-php) covers that.

```php
<?php
// send.php — PHP 8+, ext-curl (bundled)

$payload = json_encode([
    "to" => "ada@example.com",
    "subject" => "Welcome",
    "html" => "<h1>Hi {{name}}</h1>",
    "data" => ["name" => "Ada"],
    "idempotency_key" => "welcome-ada-1", // a retry replays, never re-sends
]);

$ch = curl_init("https://api.emailfast.dev/v1/emails");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 10,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("EMAILFAST_API_KEY"), // ef_sandbox_... while testing
        "Content-Type: application/json",
    ],
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($body === false || $status >= 400) {
    throw new RuntimeException("Send rejected ($status): $body");
}

$result = json_decode($body, true);
echo $result["id"] . " " . $result["status"]; // msg_... queued
```

a 202 from the API means the send is committed to a durable, partitioned outbox before we answer — a crash can't lose it, and a retry with the same idempotency key can't double-send.

## Gotchas

- Without `CURLOPT_RETURNTRANSFER`, `curl_exec` echoes the response and returns `true` — your "result" becomes garbage on the page.
- Check both failure modes: `$body === false` is a network failure, `$status >= 400` is an API rejection with a JSON body naming the reason. They need different handling.
- `CURLOPT_TIMEOUT` matters — the default is to wait on a stuck connection forever.
- Read the key with `getenv()` or `$_ENV`, never a literal that ends up in git. On Laravel, use the [Laravel guide](/send-email-with/laravel) — it wraps this same call in an injectable service.

## Next

- [PHP SDK quickstart](/docs/quickstart-php) — the typed client
- [The email API](/features/email-api) — batch, scheduling, message timelines
- [Sending from Laravel](/send-email-with/laravel)
- [All docs](/docs)
