HeyFox Send APIInhalt ↓

HeyFox Send API

Transaktions- und Systemmails per HTTP senden – Bestellbestätigungen, Passwort-Links, Rechnungen, Widerrufsbestätigungen. Über eigene Server in der Schweiz, mit Zustellstatus für jeden Empfänger.

Adressehttps://api.heyfox.email/v1
FormatJSON hinein und heraus, UTF-8, Content-Type: application/json
AnmeldungAuthorization: Bearer hfs_… – Schlüssel im Dashboard unter Zugänge → API
AbsenderJede Adresse einer im Dashboard bestätigten Domain (DKIM und Rückweg gesetzt)
Versionv1 · Stand 26.10.2026 (Webhooks). Neue Felder können dazukommen; bestehende ändern wir nicht ohne neue Version.

Jede Antwort trägt den Kopf HeyFox-Request-Id. Nenne ihn, wenn du uns wegen einer Anfrage schreibst.

Schnellstart

Drei Schritte im Dashboard: Domain hinzufügen und die zwei DNS-Einträge setzen, Freischaltung abwarten, API-Schlüssel anlegen. Dann:

curl https://api.heyfox.email/v1/messages \
  -H "Authorization: Bearer $HEYFOX_SEND_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: bestellung-1001" \
  -d '{
    "from": "Shop <bestellung@dein-shop.de>",
    "to": ["kundin@beispiel.de"],
    "subject": "Deine Bestellung 1001",
    "html": "<p>Danke für deine Bestellung!</p>",
    "text": "Danke für deine Bestellung!",
    "tags": ["bestellung"],
    "metadata": { "order_id": "1001" }
  }'

Antwort (202 Accepted):

{
  "id": "msg_8k2m4p9qa7bcx3dfr6tw2ynz",
  "status": "queued",
  "created_at": "2026-10-25T21:14:03.120Z",
  "recipients": [
    { "email": "kundin@beispiel.de", "type": "to", "status": "queued" }
  ]
}

Anmeldung mit API-Schlüssel

API-Schlüssel beginnen mit hfs_ und gehören zu genau einem Versand-Konto. Den Inhaber eines Kontos legt sie im Dashboard unter Zugänge → API an. Wir zeigen jeden Schlüssel genau einmal und speichern ihn nur als Prüfsumme – geht er verloren, leg einen neuen an und widerrufe den alten.

Schicke den Schlüssel bei jeder Anfrage im Kopf Authorization: Bearer hfs_…. Ein widerrufener Schlüssel gilt ab sofort nicht mehr.

Den Schlüssel nur auf dem Server verwenden, nie im Browser oder in einer App, die Nutzer herunterladen. Die API antwortet bewusst nicht auf Anfragen aus fremden Webseiten (kein CORS).

Mail senden

POST /v1/messages

Eine Nachricht an bis zu 50 Empfänger (an, Cc und Bcc zusammen). Alle Empfänger bekommen dieselbe Mail; Bcc-Empfänger sieht niemand. Für personalisierte Mails je Empfänger eine eigene Anfrage senden.

FeldTypBedeutung
fromPflichtAdresseAbsender. Die Domain muss im Konto bestätigt sein.
toPflichtAdresse oder ListeMindestens ein Empfänger.
cc, bccAdresse oder ListeWeitere Empfänger.
reply_toAdresse oder ListeAntwortadresse(n), höchstens 5.
subjectPflichtTextEine Zeile, höchstens 998 Zeichen.
html, textTextInhalt – mindestens eins von beiden. Am besten beides senden.
attachmentsListeHöchstens 20 Dateien, zusammen höchstens 15 MB: {"filename", "content" (Base64), "content_type", "content_id"}. Mit content_id wird die Datei ein eingebettetes Bild: im HTML als <img src="cid:logo">.
headersObjektEigene Kopfzeilen, höchstens 30, z. B. List-Unsubscribe, In-Reply-To, X-…. Nicht erlaubt sind die, die HeyFox Send selbst setzt oder die ein eigenes Feld haben: From, To, Cc, Bcc, Subject, Reply-To, Sender, Date, Message-ID, Return-Path, MIME-Version, Content-Type, Content-Transfer-Encoding, DKIM-Signature, Received, X-Tenant und die Authentifizierungs-Köpfe.
tagsListe von TextenHöchstens 10, je bis 64 Zeichen aus Buchstaben, Ziffern und _ . : -. Zum Filtern in GET /v1/messages?tag=….
metadataObjektBis zu 20 eigene Werte (Schlüssel bis 40 Zeichen, Wert bis 500 Zeichen), z. B. eure Bestellnummer. Kommt im Status zurück.

Adressen gehen als Text "kundin@beispiel.de", mit Namen "Kundin <kundin@beispiel.de>" oder als Objekt {"email": "kundin@beispiel.de", "name": "Kundin"}.

Unbekannte Felder lehnen wir ab (Fehler unknown_field) – so fällt ein Tippfehler wie htm sofort auf, statt dass eine leere Mail rausgeht.

Antwort

202 Accepted, sobald unser Mailserver die Nachricht übernommen hat – mit der Nachrichten-ID und dem Ergebnis je Empfänger:

Status je EmpfängerBedeutung
queuedÜbernommen, die Zustellung läuft.
suppressedSteht auf der Sperrliste des Kontos und wurde nicht gesendet. reason: bounce, complaint oder manual.
rejectedVom Mailserver nicht angenommen. reason: daily_limit oder recipient_rejected, dazu detail.

Wird kein einziger Empfänger angenommen, antwortet die API mit 422 all_recipients_rejected und derselben Liste.

Zustellstatus abfragen

GET /v1/messages/{id}

Der aktuelle Stand je Empfänger, so wie ihn die Server der Empfänger an uns zurückmelden. Meist steht nach wenigen Sekunden delivered da. Abrufbar 30 Tage lang.

{
  "id": "msg_8k2m4p9qa7bcx3dfr6tw2ynz",
  "status": "delivered",
  "created_at": "2026-10-25T21:14:03.120Z",
  "from_domain": "dein-shop.de",
  "tags": ["bestellung"],
  "metadata": { "order_id": "1001" },
  "recipients": [
    { "email": "kundin@beispiel.de", "type": "to", "status": "delivered", "updated_at": "2026-10-25T21:14:05.410Z" }
  ]
}

Werte in metadata kommen immer als Text zurück.

Nachrichten auflisten

GET /v1/messages?limit=20&tag=bestellung&before=msg_…

Die neuesten per API gesendeten Nachrichten zuerst. limit 1–100 (Standard 20), tag filtert, before blättert: die ID der letzten Nachricht der vorigen Seite – sie steht in next_before, solange has_more wahr ist.

{
  "data": [
    { "id": "msg_…", "status": "delivered", "created_at": "…", "from_domain": "dein-shop.de", "recipients": 1, "tags": ["bestellung"], "metadata": {} }
  ],
  "has_more": true,
  "next_before": "msg_…"
}

Konto prüfen

GET /v1/account

Für „Verbindung testen“ in eurer Software: Ist der Schlüssel gültig, ist das Konto freigeschaltet, mit welchen Domains darf gesendet werden?

{
  "id": "…", "name": "Buchhandlung Seitenwind",
  "status": "active",
  "daily_limit": 2000, "sent_last_24h": 137,
  "domains": [ { "domain": "seitenwind-buecher.de", "verified": true } ]
}

status: active, pending_release (wartet auf unsere Freischaltung), paused oder blocked.

Domains

GET /v1/domains

Alle Absender-Domains des Kontos mit Prüfstatus und den DNS-Einträgen, die dafür gesetzt sein müssen (dns_records: Typ, Name, Wert). Domains hinzufügen geht im Dashboard.

Webhooks – Ereignisse empfangen

Für jede Mail eures Kontos schicken wir euch, was mit ihr passiert ist – per POST mit JSON an eine Adresse eurer Wahl. Damit speichert eure Software den Zustellstatus selbst, z. B. als Nachweis, dass eine Widerrufsbestätigung angekommen ist. Das gilt für Mails über die API (mit message_id, tags und metadata) und über SMTP (mit smtp_message_id, der Message-ID eurer Mail).

Einrichten im Dashboard unter Einstellungen → Webhooks: Adresse (https://, aus dem Internet erreichbar), Ereignisse wählen, das Geheimnis whsec_… sicher speichern – es ist nur einmal zu sehen. „Test senden“ schickt sofort ein Ereignis webhook.test und zeigt eure Antwort. Bis zu 5 Adressen je Konto.

EreignisWann
message.deliveredDer Empfänger-Server hat die Mail angenommen.
message.deferredDer Empfänger-Server bittet um einen späteren Versuch. Einmal je Mail, beim ersten Aufschub; wir versuchen es weiter.
message.bouncedAbgelehnt (status: bounced) oder nach 24 Stunden aufgegeben (status: expired). bounce_type sagt, ob die Adresse auf die Sperrliste kam (hard) oder nicht (soft).
message.complainedDer Empfänger hat die Mail als Spam markiert. Die Adresse steht jetzt auf der Sperrliste.
webhook.testNur über „Test senden“ im Dashboard.

Aufbau eines Ereignisses

{
  "id": "evt_7k2fq9mxt4hc3wnp8db6jrse",
  "type": "message.bounced",
  "created_at": "2026-10-26T09:14:05.410Z",
  "data": {
    "account_id": "5b0c…",
    "message_id": "msg_5gq8kz2nw7hx4tcr9mfp3jdv",
    "smtp_message_id": null,
    "recipient": "kundin@beispiel.de",
    "from_domain": "dein-shop.de",
    "status": "bounced",
    "reason": "InvalidRecipient",
    "bounce_type": "hard",
    "detail": "550 5.1.1 Requested action not taken: mailbox unavailable",
    "tags": ["widerruf"],
    "metadata": { "order_id": "1001" },
    "submitted_at": "2026-10-26T09:14:01.882Z"
  }
}
FeldBedeutung
idEindeutige ID des Ereignisses – dieselbe wie im Kopf webhook-id. Nehmt sie, um doppelte Zustellungen zu erkennen.
typeSiehe Tabelle oben. Steht auch im Kopf HeyFox-Event.
created_atWann es passiert ist (UTC). Die Reihenfolge der Zustellung ist nicht garantiert – sortiert danach.
data.message_idDie msg_-ID aus POST /v1/messages; null bei SMTP-Mails.
data.smtp_message_idBei SMTP-Mails der Kopf Message-ID eurer Mail (z. B. <abc@dein-shop.de>); sonst null.
data.recipientDie Empfängeradresse. Bei mehreren Empfängern kommt je Empfänger ein eigenes Ereignis.
data.statusdelivered, deferred, bounced, expired oder complained – wie in Statuswerte.
data.reasonDie Art der Ablehnung, z. B. InvalidRecipient, QuotaIssues, SpamBlock, MessageExpired; complaint bei Beschwerden; sonst null.
data.bounce_typehard (Adresse gibt es nicht – auf der Sperrliste) oder soft (vorübergehend, z. B. Postfach voll); nur bei message.bounced.
data.detailDie Antwort des Empfänger-Servers; E-Mail-Adressen darin sind gekürzt.
data.tags, data.metadataWas ihr beim Senden über die API mitgegeben habt (Werte als Text); bei SMTP leer.
data.submitted_atWann wir die Mail angenommen haben.

Signatur prüfen

Jedes Ereignis ist signiert nach dem offenen Standard Standard Webhooks – die fertigen Bibliotheken dafür (PHP, Node, Python, Go …) funktionieren mit eurem Geheimnis whsec_… direkt. Von Hand:

webhook-idID des Ereignisses (evt_…)
webhook-timestampSekunden seit 1970 (Unix-Zeit) beim Senden
webhook-signaturev1, + Base64 von HMAC-SHA256 über {webhook-id}.{webhook-timestamp}.{Inhalt}. Schlüssel: der Teil des Geheimnisses nach whsec_, Base64-dekodiert.

Prüft gegen den unveränderten Inhalt (vor dem JSON-Dekodieren), vergleicht in konstanter Zeit und lehnt Zeitstempel ab, die mehr als 5 Minuten abweichen. Mehrere Signaturen stehen durch Leerzeichen getrennt; eine passende genügt.

// Geheimnis aus HeyFox Send (whsec_…), z. B. in wp-config.php: define('HEYFOX_WEBHOOK_SECRET', 'whsec_…');
$secret = base64_decode(substr(HEYFOX_WEBHOOK_SECRET, 6));
$body   = file_get_contents('php://input');
$id     = $_SERVER['HTTP_WEBHOOK_ID'] ?? '';
$time   = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';
$sigs   = explode(' ', $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '');

$expected = 'v1,' . base64_encode(hash_hmac('sha256', "$id.$time.$body", $secret, true));
$valid = abs(time() - (int) $time) < 300
  && count(array_filter($sigs, fn ($s) => hash_equals($expected, $s))) > 0;
if (!$valid) { http_response_code(401); exit; }

$event = json_decode($body, true);
// $event['type'], $event['data']['message_id'], $event['data']['status'] … speichern
http_response_code(200);

Zustellung und Wiederholungen

  • Antwortet mit einem Status 2xx, sobald ihr das Ereignis gespeichert habt – innerhalb von 10 Sekunden. Längere Arbeit erledigt danach. Weiterleitungen (3xx) folgen wir nicht.
  • Klappt es nicht, versuchen wir es erneut: nach 1, 5 und 30 Minuten, dann nach 2, 6, 12 und 24 Stunden (8 Versuche in knapp 2 Tagen). Im Dashboard seht ihr jeden Versuch mit eurer Antwort und könnt einzelne Ereignisse erneut senden.
  • Kommt an eine Adresse drei Tage lang nichts an, schalten wir sie ab und schreiben dem Inhaber des Kontos. Wieder einschalten geht im Dashboard.
  • Ein Ereignis kann selten doppelt kommen. Verarbeitet jede webhook-id nur einmal.
  • Ereignisse und ihr Protokoll bewahren wir 30 Tage auf. Dauerhafte Nachweise speichert ihr bei euch.

Statuswerte

EmpfängerBedeutung
queuedBei uns übernommen.
acceptedUnser Mailserver hat die Mail eingeplant.
deferredDer Empfänger-Server bittet um einen späteren Versuch. Wir versuchen es 24 Stunden lang weiter, anfangs alle 5 Minuten, später stündlich.
deliveredDer Empfänger-Server hat die Mail angenommen. Ob sie im Posteingang oder im Spam liegt, entscheidet sein Anbieter.
bouncedAbgelehnt. reason nennt die Art, z. B. InvalidRecipient (Adresse gibt es nicht), QuotaIssues (Postfach voll), SpamBlock; detail die Antwort des Servers.
expiredNach 24 Stunden mit wiederholten Versuchen aufgegeben.
complainedDer Empfänger hat die Mail als Spam markiert.
suppressed, rejectedNicht gesendet, siehe Antwort beim Senden.

Der Gesamtstatus einer Nachricht: delivered (alle zugestellt), sending oder deferred (noch unterwegs), partially_delivered (teils zugestellt, teils nicht) oder failed (an niemanden).

Statt abzufragen, könnt ihr euch jede Änderung schicken lassen: Webhooks – signiert, mit euren metadata.

Idempotency – nie doppelt senden

Netzwerke brechen ab. Damit ein erneuter Versuch keine zweite Mail auslöst, schickt beim Senden einen Kopf Idempotency-Key mit, der für diese eine Mail eindeutig ist – z. B. bestellung-1001-bestaetigung (bis 255 Zeichen).

  • Gleicher Schlüssel, gleicher Inhalt innerhalb von 24 Stunden → dieselbe Antwort wie beim ersten Mal, mit Kopf Idempotent-Replayed: true. Es geht keine zweite Mail raus.
  • Gleicher Schlüssel, anderer Inhalt → 422 idempotency_key_reused.
  • Die erste Anfrage läuft noch → 409 idempotency_in_progress. Kurz warten und wiederholen.
  • Vorübergehende Fehler (429, 5xx) merken wir uns nicht – der Versuch mit demselben Schlüssel geht danach normal durch.

Limits

Anfragenje API-Schlüssel kurz bis zu 50 auf einmal, dauerhaft 10 pro Sekunde. Jede Antwort zeigt RateLimit-Limit, RateLimit-Remaining und RateLimit-Reset (Sekunden). Darüber: 429 rate_limited mit Retry-After.
TageslimitEmpfänger je Konto in 24 Stunden, im Dashboard zu sehen. Neue Konten starten klein und wachsen mit sauberen Zahlen. Erreicht: 429 daily_limit_reached bzw. Empfänger mit reason: daily_limit.
GrößeAnfrage bis 25 MB, Anhänge zusammen bis 15 MB, 50 Empfänger je Nachricht.
AufbewahrungStatus und Empfänger 30 Tage, Inhalte gar nicht.

Sperrliste und Bounces

Gibt es eine Adresse nicht (harter Bounce) oder markiert jemand eure Mail als Spam, kommt sie automatisch auf die Sperrliste des Kontos. An gesperrte Adressen senden wir nichts mehr – auch keine Bestellmails, sonst ließen sich Newsletter als Bestellmail tarnen. Eure Software erkennt das am Status suppressed und kann die Person anders informieren.

Einträge entfernen und Adressen von Hand sperren geht im Dashboard unter Sperrliste.

Liegen in 7 Tagen mehr als 5 % harte Bounces (ab 100 Mails) oder mehr als 0,1 % Spam-Beschwerden (ab 2) vor, pausieren wir das Konto, bis die Ursache geklärt ist – das schützt die Zustellung für euch und alle anderen. Die API antwortet dann mit 403 account_paused.

Fehlercodes

Fehler kommen immer im selben Format, mit festem Code und der Meldung auf Englisch und Deutsch:

{
  "error": {
    "code": "domain_not_verified",
    "message": "The sender domain is not verified for this account. …",
    "message_de": "Die Absender-Domain ist für dieses Konto nicht bestätigt. …",
    "field": "from",
    "detail": "fremde-domain.de",
    "docs": "https://api.heyfox.email/#fehler"
  }
}
CodeHTTPWas tun
unauthorized401Schlüssel fehlt, ist falsch oder widerrufen.
invalid_json400Der Inhalt ist kein gültiges JSON.
invalid_request400Ein Feld fehlt oder ist ungültig – field und detail sagen welches.
unknown_field400Ein Feld gibt es nicht – field nennt es.
too_many_recipients400Mehr als 50 Empfänger – auf mehrere Nachrichten aufteilen.
unsupported_media_type415Als application/json senden.
payload_too_large, attachments_too_large413Anfrage über 25 MB bzw. Anhänge über 15 MB.
account_not_active403Das Konto wartet noch auf unsere Freischaltung.
account_paused, account_blocked403Pausiert bzw. gesperrt – den Grund zeigt das Dashboard.
domain_not_verified422Absender-Domain im Dashboard hinzufügen und DNS-Prüfung bestehen.
all_recipients_rejected422Kein Empfänger angenommen – siehe recipients.
idempotency_key_reused422Für eine andere Mail einen neuen Schlüssel verwenden.
idempotency_in_progress409Kurz warten, dann wiederholen.
not_found404Nachricht oder Pfad gibt es nicht (oder älter als 30 Tage).
rate_limited, daily_limit_reached429Nach Retry-After bzw. später erneut versuchen.
account_initialising, temporarily_unavailable503Vorübergehend – mit demselben Idempotency-Key wiederholen.
submission_failed, internal_error502, 500Wiederholen; bleibt es, mit der HeyFox-Request-Id melden.

Faustregel: 4xx außer 409 und 429 nicht unverändert wiederholen, 409, 429 und 5xx mit Pause und demselben Idempotency-Key wiederholen.

Lieber SMTP?

Für WordPress (FluentSMTP, WP Mail SMTP, Post SMTP), Shops und CRM-Programme geht es auch ohne Code: smtp.heyfox.email, Port 587, STARTTLS, Benutzername und Passwort eines SMTP-Zugangs aus dem Dashboard. Es gelten dieselben Regeln, die Mails erscheinen im selben Protokoll.