CeníkAPI & SMTPŘešeníSrovnáníNástrojeBlogFAQKontakt
PřihlásitRegistrovat
Tmavý režim
← Zpět na blog
26. října 2026·5 min čtení·Tým TopEmail

Odeslání e-mailu z PHP 8 přes REST API: cURL, Guzzle, přílohy, idempotence a SMTP přes PHPMailer či Symfony Mailer

Funkční příklady pro PHP 8.3: odeslání přes API čistým cURL i Guzzle, zpracování chyb podle pole code, příloha v base64, Idempotency-Key pro bezpečné opakování, a SMTP varianta přes PHPMailer a Symfony Mailer s DSN smtp://CLIENT_ID:SECRET@smtp.topemail.cz:587.

PHP aplikace má dvě cesty, jak poslat transakční e-mail přes TopEmail: HTTP požadavek na REST API, nebo SMTP přes knihovnu, kterou už pravděpodobně používáte. API dává ID e-mailu v odpovědi, strukturované chyby a idempotenci; SMTP nevyžaduje změnu kódu, jen konfigurace. Tento článek ukazuje obojí na minimálních, spustitelných příkladech pro PHP 8.3. Které zvolit, rozebírá srovnání SMTP a API.

Požadavek, který posíláte

POST https://topemail.cz/api/email/send
Authorization: Bearer CLIENT_ID:SECRET
Content-Type: application/json
Idempotency-Key: order-2026-1042-confirm

{
  "from": "Můj e-shop <objednavky@muj-eshop.cz>",
  "to": ["zakaznik@example.com"],
  "subject": "Potvrzení objednávky 2026-1042",
  "html": "<p>Děkujeme za objednávku…</p>",
  "text": "Děkujeme za objednávku…",
  "replyTo": "podpora@muj-eshop.cz",
  "tags": {"order": "2026-1042", "type": "confirm"}
}

Odpověď 202: {"ok": true, "emails": [{"id": "…", "to": "zakaznik@example.com", "status": "sent"}], "price": 0.06}. Chyba má vždy tvar {"error": "popis", "code": "strojovy_kod"}. Úplný popis polí je v OpenAPI specifikaci.

Čisté cURL bez závislostí

<?php
declare(strict_types=1);

function topemailSend(array $payload, ?string $idempotencyKey = null): array
{
    $headers = [
        'Authorization: Bearer ' . getenv('TOPEMAIL_KEY'), // "CLIENT_ID:SECRET"
        'Content-Type: application/json',
        'Accept: application/json',
    ];
    if ($idempotencyKey !== null) {
        $headers[] = 'Idempotency-Key: ' . $idempotencyKey;
    }

    $ch = curl_init('https://topemail.cz/api/email/send');
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE),
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 15,
    ]);

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

    if ($body === false) {
        throw new RuntimeException("Síťová chyba: $err");
    }

    $data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
    if ($status >= 400) {
        throw new TopEmailException($data['code'] ?? 'unknown', $data['error'] ?? 'Unknown error', $status);
    }
    return $data;
}

final class TopEmailException extends RuntimeException
{
    public function __construct(public readonly string $apiCode, string $message, int $httpStatus)
    {
        parent::__construct("$apiCode: $message", $httpStatus);
    }
}

Použití:

$result = topemailSend([
    'from'    => 'Můj e-shop <objednavky@muj-eshop.cz>',
    'to'      => ['zakaznik@example.com'],
    'subject' => 'Potvrzení objednávky 2026-1042',
    'html'    => '<p>Děkujeme za objednávku…</p>',
    'text'    => 'Děkujeme za objednávku…',
    'tags'    => ['order' => '2026-1042', 'type' => 'confirm'],
], 'order-2026-1042-confirm');

$emailId = $result['emails'][0]['id']; // uložte k objednávce

Klíč držte v proměnné prostředí nebo konfiguraci mimo repozitář. Pokud unikne, v dashboardu ho zneplatníte a vygenerujete nový.

Zpracování chyb podle code

API vrací strojový kód, na který má smysl reagovat různě. Nelogujte jen „e-mail se nepovedl":

try {
    $result = topemailSend($payload, $key);
} catch (TopEmailException $e) {
    match ($e->apiCode) {
        'invalid_recipient'   => $orders->markEmailInvalid($order),       // neopakovat, adresa je špatně
        'invalid_from',
        'domain_not_verified' => $alerts->ops('Chyba konfigurace odesílatele: ' . $e->getMessage()),
        'insufficient_credit' => $alerts->ops('Dobijte kredit TopEmail'),
        'rate_limited',
        'daily_limit',
        'provider_error'      => $queue->retryLater($payload, $key),        // dočasné, opakovat
        default               => $log->error($e->getMessage()),
    };
}

Pravidlo: chyby 4xx kromě 429 jsou chyby požadavku, opakování nepomůže. 429, 5xx a síťové chyby opakujte s rostoucím odstupem, se stejným Idempotency-Key.

Idempotency-Key: aby potvrzení neodešlo dvakrát

Worker spadne po odeslání, ale před zápisem do databáze. Nebo HTTP požadavek vyprší, ale server ho mezitím zpracoval. Bez idempotence dostane zákazník potvrzení dvakrát. S hlavičkou Idempotency-Key server opakovaný požadavek se stejným klíčem (platí 24 hodin) rozpozná a e-mail nepošle znovu, vrátí původní odpověď.

Klíč musí být stabilní pro danou událost: order-2026-1042-confirm, invoice-8812-sent, user-55-password-reset-<timestamp žádosti>. Náhodné UUID pro každý pokus idempotenci ruší. Maximální délka je 128 znaků.

Příloha: faktura v PDF

Přílohy posíláte jako pole objektů s obsahem v base64. Celková velikost požadavku je do 10 MB.

$pdf = file_get_contents('/var/app/invoices/2026-1042.pdf');

$payload['attachments'] = [[
    'filename'    => 'faktura-2026-1042.pdf',
    'content'     => base64_encode($pdf),
    'contentType' => 'application/pdf',
]];

Base64 zvětší data o třetinu, takže faktura 7 MB už neprojde. U větších souborů pošlete odkaz ke stažení.

Guzzle

Pokud už Guzzle v projektu máte, je kód kratší a výjimky dostanete zdarma:

use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;

$client = new Client([
    'base_uri' => 'https://topemail.cz',
    'timeout'  => 15,
    'headers'  => ['Authorization' => 'Bearer ' . getenv('TOPEMAIL_KEY')],
]);

try {
    $res  = $client->post('/api/email/send', [
        'headers' => ['Idempotency-Key' => 'order-2026-1042-confirm'],
        'json'    => $payload,
    ]);
    $data = json_decode((string) $res->getBody(), true, 512, JSON_THROW_ON_ERROR);
} catch (ClientException $e) {           // 4xx
    $err = json_decode((string) $e->getResponse()->getBody(), true);
    // $err['code'], $err['error']
}

ClientException pokrývá 4xx, ServerException 5xx, ConnectException síť. Posledním dvěma dejte retry.

Stav e-mailu

$res  = $client->get('/api/email/' . $emailId);
$mail = json_decode((string) $res->getBody(), true);
// $mail['status']: queued | sent | delivered | deferred | bounced | complained | rejected | failed | suppressed
// $mail['events']: pole událostí s detail a createdAt

Dotazovat se v cyklu nemusíte; stejnou informaci vám pošle webhook email.status, v PHP ověřený přes hash_equals().

SMTP varianta: PHPMailer

Když aplikace už PHPMailer používá, stačí změnit konfiguraci:

use PHPMailer\PHPMailer\PHPMailer;

$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Host       = 'smtp.topemail.cz';
$mail->Port       = 587;
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; // pro 465: ENCRYPTION_SMTPS
$mail->SMTPAuth   = true;
$mail->Username   = 'CLIENT_ID';
$mail->Password   = 'SECRET';
$mail->CharSet    = 'UTF-8';

$mail->setFrom('objednavky@muj-eshop.cz', 'Můj e-shop');
$mail->addAddress('zakaznik@example.com');
$mail->addReplyTo('podpora@muj-eshop.cz');
$mail->addCustomHeader('X-TopEmail-Tag-order', '2026-1042');
$mail->Subject = 'Potvrzení objednávky 2026-1042';
$mail->isHTML(true);
$mail->Body    = '<p>Děkujeme za objednávku…</p>';
$mail->AltBody = 'Děkujeme za objednávku…';
$mail->send();

Značky přes hlavičky X-TopEmail-Tag-<klíč> se objeví ve webhooku stejně jako pole tags z API. Server odpoví 250 2.0.0 Queued as <id>; PHPMailer tuto odpověď neexponuje přímo, ale pokud zprávě nastavíte vlastní Message-ID, relay ho použije jako idempotenční klíč, takže opakovaný pokus se stejným Message-ID e-mail nepošle znovu.

SMTP varianta: Symfony Mailer

Jeden řádek DSN, ať používáte Symfony, Laravel nebo samostatný balíček symfony/mailer:

MAILER_DSN=smtp://CLIENT_ID:SECRET@smtp.topemail.cz:587

Na portu 587 Symfony Mailer vyjedná STARTTLS automaticky; pro implicitní TLS použijte smtps://CLIENT_ID:SECRET@smtp.topemail.cz:465. Pokud secret obsahuje znaky jako @, : nebo /, zakódujte ho pomocí urlencode().

use Symfony\Component\Mailer\Transport;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mime\Email;

$mailer = new Mailer(Transport::fromDsn(getenv('MAILER_DSN')));

$email = (new Email())
    ->from('Můj e-shop <objednavky@muj-eshop.cz>')
    ->to('zakaznik@example.com')
    ->replyTo('podpora@muj-eshop.cz')
    ->subject('Potvrzení objednávky 2026-1042')
    ->text('Děkujeme za objednávku…')
    ->html('<p>Děkujeme za objednávku…</p>');
$email->getHeaders()->addTextHeader('X-TopEmail-Tag-order', '2026-1042');

$mailer->send($email);

Časté chyby

  • from mimo ověřenou doménu (invalid_from / domain_not_verified, u SMTP 550 5.7.1). Ověřte doménu a posílejte jen z ní.
  • to jako řetězec. API čeká pole, i pro jednoho příjemce.
  • Náhodný Idempotency-Key. Musí být odvozen z události.
  • Opakování po invalid_recipient. Adresa je špatně, pošlete radši SMS nebo označte objednávku.
  • Chybějící text. API ho z HTML doplní, ale vlastní verze je čitelnější.

Shrnutí

Pro nový kód použijte API: jedna funkce s cURL nebo Guzzle, chyby řešené podle code, Idempotency-Key z ID objednávky, přílohy v base64. Pro stávající kód s PHPMailerem nebo Symfony Mailerem přepište konfiguraci na smtp.topemail.cz:587 a značky předejte hlavičkami. Obě cesty končí ve stejném účtu se stejnými stavy a webhooky, za 0,06 Kč za e-mail. Přehled všech polí a stavů je v dokumentaci API.

PHPapiSMTPintegracenávodimplementace
Transakční e-maily z vaší domény

Transakční e-maily z vaší domény přes API nebo SMTP, od 0,025 do 0,06 Kč za e-mail podle objemu, prvních 1 000 e-mailů měsíčně zdarma. Registrace zdarma, bez platební karty.

Registrovat zdarma →