Skip to main content

API Documentation

The Postlark API is a small set of HTTPS endpoints that return structured email-verification verdicts. Authentication is by API key. Responses are JSON.

Base URL

All API requests use the production base URL:

https://app.postlark.io

Staging is available at https://staging.app.postlark.io for testing during integration. Both speak the same API; only the data and rate limits differ.

Authentication

Every request must include your API key in the Authorization header using the Bearer scheme:

Authorization: Bearer evk_live_…

Issue keys from the Keys page in your dashboard. Keys are shown in plaintext exactly once at creation — store them in a secrets manager. We hash keys at rest with Argon2id; if you lose a key, issue a new one and revoke the old.

Requests with a missing, malformed, revoked, or unknown key receive HTTP 401 with code unauthenticated.

POST /v1/verify

Submit a single email address; receive a structured verdict.

Request

POST /v1/verify HTTP/1.1
    Host: app.postlark.io
    Authorization: Bearer evk_live_…
    Content-Type: application/json

    {"email": "user@example.com"}

Request body

Field Type Description
email string (required) The email address to verify. UTF-8, max 254 characters.

Response (200 OK)

{
      "request_id": "GKzAVmPVTIs0oLcAABFR",
      "email": "user@example.com",
      "verdict": "deliverable",
      "confidence": "medium",
      "verified_at": "2026-05-05T18:57:10.390732Z",
      "cached": false,
      "signals": {
        "syntax_valid": true,
        "has_mx": true,
        "disposable": false,
        "role_account": false,
        "free_provider": false,
        "typo_suggestion": null,
        "smtp_reachable": null,
        "catch_all": null
      }
    }

Top-level fields

Field Type Description
request_id string Unique id for the request. Include when contacting support.
email string The address that was verified. Lowercased / trimmed exactly as evaluated.
verdict string One of deliverable, undeliverable, risky, unknown. See "Verdict semantics" below.
confidence string One of high, medium, low. Reflects how strongly the signals support the verdict; for example, an undeliverable verdict driven by a missing MX record is high, while a deliverable verdict without an SMTP probe is medium.
verified_at string (ISO 8601) UTC timestamp of when the verdict was first produced. For cached: true responses this is the original verification time, not the time of the cache hit.
cached boolean true when the verdict came from the result cache (default 7-day TTL); false for a freshly computed verdict.
signals object Per-signal breakdown — see next table.

Signals

Field Type Description
syntax_valid boolean Whether the address parses per RFC 5322 plus our heuristics.
has_mx boolean Whether the domain has resolvable MX records.
disposable boolean Whether the domain matches a community-curated disposable list (Mailinator, 10MinuteMail, etc.).
role_account boolean Whether the local part is a typical role account (info@, support@, sales@, …).
free_provider boolean Whether the domain is a major free email provider (gmail.com, yahoo.com, outlook.com, …).
typo_suggestion string | null A suggested correction (e.g., gmial.comgmail.com) when our typo detector matches; otherwise null.
smtp_reachable boolean | null Whether the recipient's MX accepted a RCPT TO probe. null when SMTP probing is disabled or skipped. SMTP probing is currently disabled on all v1 plans (deferred to v2).
catch_all boolean | null Whether the domain accepts mail to any local part. null when SMTP probing is disabled or the verdict is otherwise undetermined.

Verdict semantics

  • deliverable — The address looks legitimate: valid syntax, an MX record, and not flagged by any negative signal.
  • undeliverable — At least one strong negative signal: invalid syntax, no MX, or a high-confidence typo of a known good domain.
  • risky — The address is technically deliverable but carries warning signals (disposable domain, role account). You may want to challenge or rate-limit before relying on it.
  • unknown — We couldn't form a confident verdict (transient DNS issues, etc.). Retrying after a short delay is reasonable.

GET /v1/account

Returns the authenticated account's plan, current-period usage, and quota.

GET /v1/account HTTP/1.1
    Host: app.postlark.io
    Authorization: Bearer evk_live_…

Response (200 OK)

{
      "request_id": "GKzAWrKrcTI0oLcAABJB",
      "plan": "scale",
      "plan_quota": 500000,
      "status": "active",
      "current_period_start": "2026-05-01T00:00:00Z",
      "current_period_end": "2026-06-01T00:00:00Z",
      "usage": {
        "verifications_count": 12345,
        "overage_count": 0
      }
    }

Fields

Field Type Description
plan string One of free, starter, growth, scale, payg.
plan_quota integer Verifications included this billing period. 0 for PAYG (everything is metered).
status string | null Subscription status: active, past_due, canceled, etc. null on the free tier.
current_period_start string (ISO 8601) | null Start of the current Stripe billing period. null on the free tier.
current_period_end string (ISO 8601) | null End of the current Stripe billing period. null on the free tier.
usage.verifications_count integer Total verifications in the current period.
usage.overage_count integer Verifications above plan_quota in the current period (paid plans only).

GET /v1/health

Liveness check. No authentication required.

GET /v1/health HTTP/1.1
    Host: app.postlark.io

Returns 200 with {"status":"ok","request_id":"…"} when the service is up.

Errors

Errors return a non-2xx status and a JSON body of consistent shape:

{
      "error": {
        "code": "unauthenticated",
        "message": "Invalid API key"
      },
      "request_id": "GKxx5TjekFhpggYAAAUi"
    }

Some errors include a details object inside error for additional context (e.g., the field that failed validation). Clients should not assume details is present.

Status Code When
400 invalid_request Malformed JSON, missing required field, or invalid value (including syntactically invalid email).
401 unauthenticated API key missing, malformed, revoked, or unknown.
403 forbidden Authenticated but not authorized for the requested resource.
404 not_found Path doesn't exist.
429 rate_limit_exceeded Too many requests in a short window. Retry-After header included.
429 quota_exceeded Monthly quota exceeded (free tier).
500 internal_error Unexpected server error. Include the request_id when reporting.

Rate limits & quotas

Each plan has a monthly verification quota that resets at the start of your Stripe billing period. The free tier additionally enforces a per-minute rate limit (10 requests/min) to protect SMTP-probe IP reputation; paid plans have no per-minute cap.

Plan Monthly quota Per-minute cap Overage
Free 100 10 429 quota_exceeded
Starter 10,000 uncapped billed at plan rate
Growth 100,000 uncapped billed at plan rate
Scale 500,000 uncapped billed at plan rate
PAYG n/a uncapped every call billed at plan rate

Request IDs

Every response includes a unique request_id in the JSON body. Include it whenever you contact support — it lets us trace the exact call through our logs.