# Quickstart: PHP

> Send email from PHP with no dependencies: a stream-context POST to the Email Fast REST API with a sandbox key, an idempotency key, and the event timeline.

Canonical: https://emailfast.dev/docs/quickstart-php

## 1. Send

The API is plain JSON over HTTPS with bearer auth. `file_get_contents` with a
stream context does it in stock PHP — nothing to install. (If you prefer the
curl extension or a generated PHP SDK, both speak to the same two endpoints —
see [the docs](/docs).)

```php
<?php
$apiKey = getenv("EMAILFAST_API_KEY"); // ef_sandbox_… works everywhere

$body = json_encode([
    "to" => "ada@example.com",
    "subject" => "Welcome to the ledger",
    "html" => "<h1>Hi {{name}}</h1><p>Your account is ready.</p>",
    "data" => ["name" => "Ada"],
    "idempotency_key" => "welcome-ada-1", // retry-safe: same key can never double-send
]);

$ctx = stream_context_create(["http" => [
    "method" => "POST",
    "header" => "Authorization: Bearer $apiKey\r\nContent-Type: application/json",
    "content" => $body,
]]);

$msg = json_decode(file_get_contents("https://api.emailfast.dev/v1/emails", false, $ctx), true);
echo $msg["id"], " ", $msg["status"], PHP_EOL; // 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. That is what the `idempotency_key` buys you: put this call
behind any retry logic and a timeout after the server committed the send still
can't produce a duplicate.

## 2. Watch it happen

```php
<?php
$ctx = stream_context_create(["http" => [
    "header" => "Authorization: Bearer " . getenv("EMAILFAST_API_KEY"),
]]);

$timeline = json_decode(
    file_get_contents("https://api.emailfast.dev/v1/messages/" . $msg["id"], false, $ctx),
    true
);

foreach ($timeline["events"] as $e) {
    echo $e["type"], PHP_EOL; // admitted → rendered → (sandbox-)delivered …
}
```

With a sandbox key the message lands in your hosted capture inbox — same
pipeline, nothing sent: sandbox keys (ef_sandbox_…) that run the real pipeline dry: real validation, real rendering, real events, a hosted capture inbox — and no email leaves.

## Notes for production PHP

- Set `ignore_errors => true` in the context if you want to read the JSON error
  body on a 4xx instead of a PHP warning.
- The same two endpoints — `POST /v1/emails`, `GET /v1/messages/:id` — are all a
  queue worker or a WordPress hook needs.

## Next

- [The raw REST reference](/docs/quickstart-curl)
- [Templates & MJML](/features/templates)
- [Migrating from SendGrid/Mailgun/Postmark](/features/migration)
