/php Temp Email API
Create disposable inboxes, receive verification emails, and extract OTP codes from PHP — with a small Guzzle client, a runnable PHPUnit test, a zero-dependency cURL version, and a Laravel snippet.
Testing email-dependent flows in PHP — Laravel or Symfony signups, password resets, magic links — usually stalls on one thing: the verification email lands in a mailbox your test can't read. The OpenInbox API solves that with a disposable inbox you create and poll over plain HTTP. This guide gives you a reusable Guzzle client, a PHPUnit integration test, a dependency-free cURL variant, and the Laravel equivalent. Every call targets the real OpenInbox API.
How the flow works
- Create a disposable inbox: POST /api/inbox (no auth) → ["id" => ..., "email" => ..., "expiresAt" => ...].
- Use that address in your signup/reset form so your app emails it.
- Poll GET /api/inbound/api/emails?inboxEmail=<email> with the X-API-KEY header (premium) until an email arrives.
- preg_match a 4–8 digit code (or a URL) out of the email textBody.
- Submit the code / visit the link and assert the result.
Setup
Guzzle is the most common PHP HTTP client; install it with Composer. Polling the inbox is a premium endpoint, so read the key from the environment rather than committing it. (Prefer no dependencies? The cURL version further down uses only built-in functions.)
composer require guzzlehttp/guzzle
export OPENINBOX_API_KEY="your-premium-api-key"A reusable OpenInbox client (Guzzle)
Wrap the two calls you need — create and poll — in a small class. createInbox returns the decoded inbox; waitForOtp polls the inbox and returns the first 4–8 digit code it finds in textBody. Full URLs are used so the routing is unambiguous.
<?php
namespace Tests\Support;
use GuzzleHttp\Client;
use RuntimeException;
final class OpenInbox
{
private string $base = 'https://api.openinbox.io/api';
private Client $http;
private string $apiKey;
public function __construct(?string $apiKey = null)
{
$this->apiKey = $apiKey ?? (string) getenv('OPENINBOX_API_KEY');
$this->http = new Client(['http_errors' => false]);
}
/** Create a disposable inbox (no auth required). */
public function createInbox(): array
{
$res = $this->http->post($this->base . '/inbox');
return json_decode((string) $res->getBody(), true); // ['id','email','expiresAt',...]
}
/** Poll until an email arrives, then return the OTP from textBody. */
public function waitForOtp(string $inboxEmail, int $timeout = 30): string
{
$deadline = time() + $timeout;
while (time() < $deadline) {
$res = $this->http->get($this->base . '/inbound/api/emails', [
'query' => ['inboxEmail' => $inboxEmail],
'headers' => ['X-API-KEY' => $this->apiKey],
]);
$emails = json_decode((string) $res->getBody(), true)['emails'] ?? [];
if ($emails && preg_match('/\b\d{4,8}\b/', $emails[0]['textBody'] ?? '', $m)) {
return $m[0];
}
sleep(2);
}
throw new RuntimeException('No OTP email arrived in time');
}
}Using it in a PHPUnit test
With the client in place, an integration test reads like the user story: create an inbox, trigger your signup, wait for the code, and assert. Swap the cURL trigger for a call to your own application or HTTP client.
<?php
use PHPUnit\Framework\TestCase;
use Tests\Support\OpenInbox;
final class SignupVerificationTest extends TestCase
{
public function test_user_verifies_email_with_otp(): void
{
$openInbox = new OpenInbox();
$inbox = $openInbox->createInbox();
// Trigger your app so it emails the verification code
$ch = curl_init('https://staging.yourapp.com/api/register');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'email' => $inbox['email'],
'password' => 'Str0ng!Pass',
]),
]);
curl_exec($ch);
// Pull the OTP from the disposable inbox
$otp = $openInbox->waitForOtp($inbox['email']);
$this->assertMatchesRegularExpression('/^\d{4,8}$/', $otp);
// …submit $otp to your verify endpoint and assert the success state
}
}No dependencies? Use built-in cURL
If you would rather not pull in Guzzle, the built-in cURL functions do the same job. This standalone script creates an inbox and polls until the code arrives.
<?php
$base = 'https://api.openinbox.io/api';
$apiKey = getenv('OPENINBOX_API_KEY');
// 1. Create an inbox (no auth required)
$ch = curl_init("$base/inbox");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true]);
$inbox = json_decode(curl_exec($ch), true);
echo "Inbox: {$inbox['email']}" . PHP_EOL;
// 2. Poll for the email (X-API-KEY required)
for ($i = 0; $i < 15; $i++) {
sleep(2);
$url = "$base/inbound/api/emails?inboxEmail=" . urlencode($inbox['email']);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["X-API-KEY: $apiKey"],
]);
$emails = json_decode(curl_exec($ch), true)['emails'] ?? [];
if ($emails) {
preg_match('/\b\d{4,8}\b/', $emails[0]['textBody'], $m);
echo "OTP: {$m[0]}" . PHP_EOL;
break;
}
}Laravel
In a Laravel app, use the Http facade instead of Guzzle directly: Http::post("https://api.openinbox.io/api/inbox")->json() creates the inbox, and Http::withHeaders(["X-API-KEY" => config("services.openinbox.key")])->get("https://api.openinbox.io/api/inbound/api/emails", ["inboxEmail" => $email])->json("emails") polls it. Extract the OTP from $emails[0]["textBody"] with preg_match exactly as above. Keep the key in config/services.php, sourced from the OPENINBOX_API_KEY env var.
Symfony
In Symfony, inject HttpClientInterface and make the same two calls. $client->request("POST", "https://api.openinbox.io/api/inbox")->toArray() creates the inbox; $client->request("GET", "https://api.openinbox.io/api/inbound/api/emails", ["query" => ["inboxEmail" => $email], "headers" => ["X-API-KEY" => $apiKey]])->toArray()["emails"] polls it. Extract the OTP from $emails[0]["textBody"] with preg_match, just like the Guzzle client. The endpoints, the X-API-KEY header, and the textBody field are identical — only the HTTP client wrapper changes.
Common pitfalls
- •Always bound the polling loop (a for-loop or a deadline) — an unbounded while will hang your test suite if the email never arrives.
- •Creating an inbox needs no auth, but GET /api/inbound/api/emails is premium. A 401/403 means the X-API-KEY header is missing or the account is not on a paid plan.
- •Read $email["textBody"] — there is no pre-parsed code field. preg_match("/\b\d{4,8}\b/", ...) grabs a 4–8 digit run; tighten it if the body has other numbers.
- •Guzzle throws on 4xx/5xx by default; set http_errors => false (as shown) so you can inspect the body and retry instead of catching exceptions in the loop.
- •urlencode() the inbox address when building the poll URL by hand so a + or . in the local part is transmitted correctly.
Frequently Asked Questions
How do I get a temporary email in PHP?
Send a POST request to https://api.openinbox.io/api/inbox (no auth) — with Guzzle, the Http facade, or built-in cURL — and read the returned email field. The response also includes id and expiresAt.
Do I need an API key?
Creating an inbox is unauthenticated. Polling received emails (GET /api/inbound/api/emails) requires a premium X-API-KEY, which you should read from the OPENINBOX_API_KEY environment variable.
How do I extract the OTP code in PHP?
Run preg_match("/\b\d{4,8}\b/", $email["textBody"], $m) against the email body returned by the poll endpoint, then use $m[0]. There is no separate parsed field — it is a one-line regex on textBody.
Does this work with Laravel or Symfony?
Yes. In Laravel use the Http facade; in Symfony use HttpClientInterface. The endpoints, headers, and textBody field are identical — only the HTTP client wrapper changes.
Can I run this in PHPUnit and CI?
Yes — the examples are written as a PHPUnit integration test and only make HTTPS calls, so they run on any CI runner. Store the key as a secret exposed via OPENINBOX_API_KEY.
How long does a disposable inbox last?
Free inboxes expire after one hour, which is plenty for a test run. Create a fresh inbox per test so parallel tests never share a mailbox.
Related Pages
Explore More
Your private inbox is one click away
Free, instant, no registration needed. Works with any service. Expires automatically after 1 hour.
