API7 min read

API response format — JSON schema and pagination

The response envelope, the JSON schema and the pagination model — reference documentation you can paste straight into your codegen.

Every REST API has three shape decisions that determine whether the client code will be pleasant or miserable: the envelope, the pagination model, and the error format. This page is the reference for all three, with a JSON Schema fragment you can paste into your OpenAPI generator.

Spec highlights

Envelope is { data, error, meta } on every response. Pagination is cursor-based with Link headers (RFC 5988). Timestamps are ISO 8601 with explicit UTC. Errors always carry a requestId you can cite in support tickets.

Response envelope

Every JSON response — success or error — has the same top-level shape:

{
  "data":  <object | array | null>,
  "error": <object | null>,
  "meta":  <object>
}

On success, data is populated and error is null. On failure, the reverse. meta is always present and always contains at least a requestId.

Success example

{
  "data": {
    "id": "t_01H9XABC4D5E6F7G8H",
    "status": "complete",
    "summary": { "inboxCount": 18, "spamCount": 3, "missingCount": 1, "total": 22 }
  },
  "error": null,
  "meta": {
    "requestId":        "req_01H9X7KEYABCDEF",
    "creditsRemaining": 248,
    "version":          "2026-07-01"
  }
}

Error example

{
  "data": null,
  "error": {
    "code":    "validation_error",
    "message": "Field 'from' must be a valid email address",
    "field":   "from",
    "docs":    "https://check.live-direct-marketing.online/docs/errors#validation_error"
  },
  "meta": {
    "requestId": "req_01H9X7KEYBADBEEF",
    "version":   "2026-07-01"
  }
}

Status code map

  • 200 OK — successful read.
  • 202 Accepted — test created, running asynchronously. Follow the Location header or poll the returned id.
  • 400 Bad Request — malformed JSON or missing required field. Do not retry.
  • 401 Unauthorized — missing or invalid Bearer key. Do not retry.
  • 403 Forbidden — authenticated but not allowed (for example, reading another account's test). Do not retry.
  • 404 Not Found — test ID does not exist or has been purged past retention.
  • 409 Conflict — idempotency collision with a different payload body.
  • 422 Unprocessable Entity — request is well-formed but rejected by domain logic (e.g. sender domain blocked).
  • 429 Too Many Requests — rate limit hit. Read Retry-After.
  • 5xx — upstream or internal failure. Retry with exponential backoff.

Pagination — cursor-based

Every list endpoint uses forward-only cursor pagination. You pass ?limit=N&cursor=..., and receive meta.nextCursor (or null) plus a Link header with rel="next".

GET /api/tests?limit=50

200 OK
Link: <https://check.live-direct-marketing.online/api/tests?limit=50&cursor=eyJpZCI6InRfMDEifQ==>; rel="next"

{
  "data": [ /* 50 test objects */ ],
  "error": null,
  "meta": {
    "nextCursor": "eyJpZCI6InRfMDEifQ==",
    "hasMore":    true,
    "requestId":  "req_..."
  }
}

Why cursor, not page?

Page-based pagination breaks when items are inserted or deleted mid-walk — you can either miss items or see duplicates. Cursor pagination is stable under concurrent writes and is the model every serious REST API has converged on over the last decade.

The cursor is opaque — do not try to base64-decode it or sort by it. Treat it as a "continue reading here" token and nothing more.

Test object schema

{
  "id":              "t_01H9XABC4D5E6F7G8H",
  "status":          "complete",
  "createdAt":       "2026-07-03T11:02:14Z",
  "completedAt":     "2026-07-03T11:03:42Z",
  "from":            "hello@news.yourbrand.com",
  "subject":         "Weekly digest #42",
  "summary": {
    "inboxCount":    18,
    "spamCount":      3,
    "missingCount":   1,
    "total":         22
  },
  "auth":            { "spf": "pass", "dkim": "pass", "dmarc": "pass" },
  "spamAssassinScore": 1.2,
  "rspamdScore":       0.8,
  "providers":       [ /* per-seed detail */ ],
  "dnsbl":           [ /* blacklist check results */ ],
  "screenshots":     [ /* screenshot URLs */ ],
  "metadata":        { /* opaque tenant metadata */ }
}

Provider result sub-object

{
  "provider":  "gmail",
  "folder":    "inbox",
  "folderRaw": "INBOX",
  "tookMs":    4120,
  "headersUrl": "https://check.live-direct-marketing.online/r/t_.../headers.txt",
  "screenshotUrl": "https://check.live-direct-marketing.online/r/t_.../gmail.png"
}

folder is normalised to a small enum: inbox, spam, promotions, updates, social, missing, other. folderRaw is the provider-native label for debugging.

SPF / DKIM / DMARC sub-object

{
  "spf": {
    "result": "pass",
    "record": "v=spf1 include:_spf.google.com include:sendgrid.net ~all",
    "domain": "news.yourbrand.com"
  },
  "dkim": {
    "result":   "pass",
    "selector": "s1",
    "domain":   "news.yourbrand.com",
    "keySize":  2048
  },
  "dmarc": {
    "result":   "pass",
    "policy":   "quarantine",
    "alignment": { "spf": "pass", "dkim": "pass" }
  }
}

DNSBL sub-object

[
  { "list": "zen.spamhaus.org",    "listed": false },
  { "list": "bl.spamcop.net",      "listed": false },
  { "list": "b.barracudacentral.org", "listed": false },
  { "list": "dnsbl.sorbs.net",     "listed": true, "reason": "dynamic IP range" }
]

Screenshot URL lifecycle

Screenshot URLs are signed and time-limited. They survive as long as the test is within its retention window (30 days on free, 12 months on Starter, 24 on Growth), then return 404. The URL does not change — there is no rotation — so you can bookmark or embed it as long as the test is in retention.

Do not hotlink screenshots into tenant UI long-term. Download them and rehost if you need longer retention than your plan provides.

Timestamps — ISO 8601, always UTC

Every timestamp the API returns is ISO 8601, in UTC, with a trailing Z. Example: 2026-07-03T11:02:14Z. No other format is ever returned; no timezone offsets other than Z; no epoch seconds; no microseconds.

Render in the user's local timezone on your frontend using whatever library you already have. Never assume a server-side format change.

JSON Schema fragment

Paste this into your OpenAPI generator. It covers the envelope and the test object with enum-validated fields.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id":     "https://check.live-direct-marketing.online/schema/test.json",
  "type":    "object",
  "required": ["data", "error", "meta"],
  "properties": {
    "data": {
      "oneOf": [
        { "type": "null" },
        {
          "type": "object",
          "required": ["id", "status", "createdAt"],
          "properties": {
            "id":         { "type": "string", "pattern": "^t_[A-Z0-9]{26}$" },
            "status":     { "enum": ["pending", "running", "complete", "cancelled", "failed"] },
            "createdAt":  { "type": "string", "format": "date-time" },
            "completedAt":{ "type": "string", "format": "date-time" },
            "from":       { "type": "string", "format": "email" },
            "subject":    { "type": "string", "maxLength": 998 },
            "summary": {
              "type": "object",
              "required": ["inboxCount", "spamCount", "missingCount", "total"],
              "properties": {
                "inboxCount":   { "type": "integer", "minimum": 0 },
                "spamCount":    { "type": "integer", "minimum": 0 },
                "missingCount": { "type": "integer", "minimum": 0 },
                "total":        { "type": "integer", "minimum": 0 }
              }
            },
            "auth": {
              "type": "object",
              "properties": {
                "spf":   { "enum": ["pass", "fail", "neutral", "softfail", "none"] },
                "dkim":  { "enum": ["pass", "fail", "none"] },
                "dmarc": { "enum": ["pass", "fail", "none"] }
              }
            }
          }
        }
      ]
    },
    "error": {
      "oneOf": [
        { "type": "null" },
        {
          "type": "object",
          "required": ["code", "message"],
          "properties": {
            "code":    { "type": "string" },
            "message": { "type": "string" },
            "field":   { "type": "string" },
            "docs":    { "type": "string", "format": "uri" }
          }
        }
      ]
    },
    "meta": {
      "type": "object",
      "required": ["requestId"],
      "properties": {
        "requestId":        { "type": "string" },
        "creditsRemaining": { "type": "integer" },
        "nextCursor":       { "type": "string" },
        "hasMore":          { "type": "boolean" },
        "version":          { "type": "string" }
      }
    }
  }
}
Version header

The meta.version field echoes the API version your request was handled under. Pin to a version by sending X-API-Version: 2026-07-01. Without the header you get the latest stable version, which can introduce additive changes (new fields) but never breaking ones.

Frequently asked questions

Will you ever change a response format?

We make additive changes freely — new fields, new status codes in the 2xx range. Breaking changes (removed fields, changed types, new required request fields) ship under a new X-API-Version. Old versions stay supported for at least 12 months after deprecation.

Why cursor pagination instead of offset?

Offset pagination gives you duplicate or skipped items when rows are inserted between page fetches. Cursor pagination is stable under concurrent writes. It also performs better on the database side, which matters once history grows.

Are status values stable enums I can switch on?

Yes. The status field is a closed enum: pending, running, complete, cancelled, failed. New values would require a new major API version. You can safely switch on it in your code.

Can I get JSON:API or HAL instead of this envelope?

No. We picked one envelope and stuck with it. JSON:API adds indirection most clients do not need; HAL's hypermedia links are neat but every client library ignores them. Our envelope is simple enough to wrap in any language in five minutes.
Related reading

Check your deliverability across 20+ providers

Gmail, Outlook, Yahoo, Mail.ru, Yandex, GMX, ProtonMail and more. Real inbox screenshots, SPF/DKIM/DMARC, spam engine verdicts. Free, no signup.

Run Free Test →

Unlimited tests · 20+ seed mailboxes · Live results · No account required