API documentation

With the Postwacht API you check your emails automatically – for example after every deployment or before every newsletter. The API is free and enabled on request.

Quick start

  1. Apply for access in your account under API. Once approved – usually within one business day – you create a key there.
  2. Create a test. You'll get back a one-time address:
    curl -X POST https://postwacht.de/api/v1/tests \
      -H "Authorization: Bearer pwk_…"
    
    {"code": "k7m2x9…", "address": "k7m2x9…@in.postwacht.de", "status": "waiting", …}
  3. Send your email to this address – from your application, your newsletter tool or your mail server.
  4. Poll the result until status is done (usually after 10 to 30 seconds):
    curl https://postwacht.de/api/v1/tests/k7m2x9… \
      -H "Authorization: Bearer pwk_…"

Authentication and limits

Every request needs the header Authorization: Bearer <key>. A key has the same rights as your account. You can revoke it in your account at any time.

In your application you state which servers your application calls the API from (hostname, IP address or small network). Requests from other addresses are rejected – even with a valid key.

Tests

CallPurpose
POST /api/v1/testsCreate a new test. Response: code, address, status, report_url
GET /api/v1/tests?limit=20Your latest tests
GET /api/v1/tests/{code}Result of a test with score, all checks and fix plan

status is waiting (email not there yet), received, analyzing, done or failed. With done, the response contains score (0 to 10), results with every single check and fix_plan with the recommended actions.

Example of a check in a CI pipeline:

SCORE=$(curl -s https://postwacht.de/api/v1/tests/$CODE \
  -H "Authorization: Bearer $POSTWACHT_KEY" | jq .score)
[ "$(echo "$SCORE >= 9" | bc)" = 1 ] || exit 1

Titles and findings in responses are in German by default. Send the header Accept-Language: en to get them in English.

Domains and DMARC

CallPurpose
GET /api/v1/domainsMonitored domains with status and DMARC report address
POST /api/v1/domainsMonitor a domain. Body: {"domain": "example.com"}
GET /api/v1/domains/{domain}?days=30Last check, DMARC analysis (volume, pass rate, senders, daily values) and TLS-RPT analysis

Inbox test

CallPurpose
POST /api/v1/inbox-testsStart an inbox test. Response: code (put it in the subject or body) and addresses (recipients)
POST /api/v1/inbox-tests/{code}/cancelCancel a running inbox test. If no mailbox has found the email yet, it doesn't count against the daily limit.
GET /api/v1/inbox-tests/{code}Folder per provider: inbox, promotions, spam, missing, cancelled or null (still checking)

Webhooks

Instead of polling, you can have events sent to you. Add an HTTPS address under Account → API. Postwacht then sends a POST with JSON:

{"event": "test.done",
 "created_at": "2026-10-11T09:03:56+00:00",
 "data": {"code": "k7m2x9…", "status": "done", "score": 9.4,
          "from_domain": "example.com", "report_url": "https://postwacht.de/t/k7m2x9…"}}
EventWhenContent of data
test.doneA test has been analyzedcode, status, score, subject, from_domain, report_url
domain.changedA result of the daily domain check has changeddomain, status, changes (check, before, after, details), url
inbox_test.doneAn inbox test has completedcode, results (provider and folder), url
pingYou click “Send test”message

Verifying the signature

Every call carries the headers X-Postwacht-Event, X-Postwacht-Timestamp and X-Postwacht-Signature. The signature is an HMAC-SHA256 over <timestamp>.<body> with your secret. Verify it and discard calls whose timestamp is older than five minutes:

import hashlib, hmac, time

def is_genuine(secret: str, timestamp: str, body: bytes, signature: str) -> bool:
    if abs(time.time() - int(timestamp)) > 300:
        return False
    mac = hmac.new(secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256)
    return hmac.compare_digest("sha256=" + mac.hexdigest(), signature)

Respond with a status between 200 and 299. For other responses or if your address is unreachable, Postwacht retries after 1, 5 and 30 minutes. After 20 consecutive failures the webhook is disabled; “Send test” turns it back on. Redirects are not followed, and only public addresses are called.

Error

Errors come as JSON with a matching HTTP status, e.g. {"error": "Daily limit of 20 tests reached"}.

StatusMeaning
401Key missing, wrong or revoked
403API access not (or no longer) enabled, request from an address that isn't allowed, or account blocked
404Test or domain doesn't exist (or doesn't belong to your account)
409Maximum number of domains reached
422Invalid input
429Limit reached – for the per-minute limit, the Retry-After header says when you can continue