Quick start

The API returns JSON over HTTPS. Every data endpoint uses GET and is read-only. Its base address on this site is https://securemark.leadify.biz/api/v1.

  1. Choose a plan that includes API access. An organization owner or admin enables it from API access in the side menu.
  2. Create a named key, select its read permissions, and choose the domains it can access. For the full walkthrough, select all three read permissions.
  3. Save the full key when it is shown. It cannot be retrieved later.
  4. Call /access to confirm your access, then /domains to discover domain IDs.

The examples below read SECUREMARK_BASE_URL and SECUREMARK_API_KEY from your shell environment. Set the base URL to https://securemark.leadify.biz, without a trailing slash, and supply the key from your secret manager.

cURL

curl --fail-with-body \
  -H "Authorization: Bearer $SECUREMARK_API_KEY" \
  "$SECUREMARK_BASE_URL/api/v1/access"

PowerShell

Invoke-RestMethod -Uri "$env:SECUREMARK_BASE_URL/api/v1/access" -Headers @{ Authorization = "Bearer $env:SECUREMARK_API_KEY" }

Next, request /api/v1/domains, copy a domain's id, and use it in /api/v1/domains/{domainId}/summary. Set DOMAIN_ID to that value for the pagination example below. To export report details, list the domain's reports and use each report's public id in the records endpoint.

API keys and permissions

Send Authorization: Bearer <api-key> on every data request. Keep the key out of URLs and source control. Browser login cookies do not authenticate the public API, and an API key cannot sign into browser or admin routes.

Read permissions selected when creating a key
PermissionAllows
domains:readList domains and read their stored policy and status.
analytics:readRead summary counts, daily counts and sending sources.
reports:readRead report metadata and the records in each report.

Each key belongs to one member and one organization. Its access is limited by both its selected domains and that member's current permissions. A key with no selected domains cannot read domain data. An explicit all-domains grant includes future domains the member can access; Client members can only select their assigned domains.

Keys do not expire automatically. Rotate a key to issue a replacement; the previous key remains valid for up to 24 hours. Update your integration, then revoke the previous key. Revocation, the end of a rotation overlap, disabled membership, loss of SecureMark access, or loss of a domain assignment stops the corresponding access. Owners and admins can revoke organization keys; other members manage their own.

Endpoint reference

All paths below are relative to https://securemark.leadify.biz. Replace IDs in braces with values returned by the API. The sample IDs, domains, addresses, dates and counts are fictional; use a date range within your own available history.

All nine read endpoints
GET endpointRead permission
/api/v1/accessAny valid API key
/api/v1/domainsdomains:read
/api/v1/domains/{domainId}domains:read
/api/v1/domains/{domainId}/summaryanalytics:read
/api/v1/domains/{domainId}/timeseriesanalytics:read
/api/v1/domains/{domainId}/sourcesanalytics:read
/api/v1/domains/{domainId}/reportsreports:read
/api/v1/reports/{reportId}reports:read
/api/v1/reports/{reportId}/recordsreports:read

Check your access

GET /api/v1/access

Identify the organization and read permissions attached to your key, the earliest report date you can read, and the current query limits.

Required permission: A valid API key; no read scope is required.

No query parameters. A null availableFrom means there is no plan history cutoff.

Example 200 response

{
  "organizationId": "00000000-0000-4000-8000-000000000001",
  "scopes": [
    "domains:read",
    "analytics:read",
    "reports:read"
  ],
  "availableFrom": "2026-10-01T00:00:00Z",
  "requestsPerKeyPerMinute": 60,
  "requestsPerOrganizationPerMinute": 300,
  "maxAnalyticsDays": 90,
  "maxPageSize": 500
}

List your domains

GET /api/v1/domains

Discover the domain IDs your integration can use. The list is limited to the domains granted to your key and currently available to its owner.

Required permission: domains:read

Optional: limit, cursor. The example shows one complete page.

Example 200 response

{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000002",
      "name": "example.com",
      "verified": true,
      "paused": false,
      "policyObservedAt": "2026-10-01T00:00:00Z",
      "policy": {
        "p": "none",
        "sp": null,
        "np": null,
        "pct": "100",
        "adkim": "r",
        "aspf": "r"
      }
    }
  ],
  "nextCursor": null
}

Read a domain

GET /api/v1/domains/{domainId}

Read a domain's name, verification and pause status, and its last stored DMARC policy observation. This request does not perform a fresh DNS lookup.

Required permission: domains:read

Replace domainId with an id returned by the domains list. A paused domain's metadata remains available.

Example 200 response

{
  "id": "00000000-0000-4000-8000-000000000002",
  "name": "example.com",
  "verified": true,
  "paused": false,
  "policyObservedAt": "2026-10-01T00:00:00Z",
  "policy": {
    "p": "none",
    "sp": null,
    "np": null,
    "pct": "100",
    "adkim": "r",
    "aspf": "r"
  }
}

Read DMARC totals

GET /api/v1/domains/{domainId}/summary

Get message totals, aligned DMARC results and receiver dispositions for a domain over a date range.

Required permission: analytics:read

Optional: from, to. This example covers 2026-10-01 through, but not including, 2026-10-02.

Example 200 response

{
  "domainId": "00000000-0000-4000-8000-000000000002",
  "from": "2026-10-01T00:00:00Z",
  "to": "2026-10-02T00:00:00Z",
  "latestReportReceivedAt": "2026-10-02T06:00:00Z",
  "counts": {
    "messages": 1000,
    "dmarcPassed": 970,
    "dmarcFailed": 20,
    "dmarcUnknown": 10,
    "dispositionNone": 980,
    "dispositionQuarantine": 15,
    "dispositionReject": 5,
    "dispositionUnknown": 0
  }
}

Read daily totals

GET /api/v1/domains/{domainId}/timeseries

Get the same counts for every UTC day in a date range. Days without data contain zero counts.

Required permission: analytics:read

Optional: from, to. Every day is returned in one response; this endpoint does not use cursors.

Example 200 response

{
  "domainId": "00000000-0000-4000-8000-000000000002",
  "from": "2026-10-01T00:00:00Z",
  "to": "2026-10-02T00:00:00Z",
  "data": [
    {
      "day": "2026-10-01T00:00:00Z",
      "counts": {
        "messages": 1000,
        "dmarcPassed": 970,
        "dmarcFailed": 20,
        "dmarcUnknown": 10,
        "dispositionNone": 980,
        "dispositionQuarantine": 15,
        "dispositionReject": 5,
        "dispositionUnknown": 0
      }
    }
  ]
}

Read sending sources

GET /api/v1/domains/{domainId}/sources

Group the domain's message counts by sending IP address. Reverse DNS, network and country details are stored observations and may be null.

Required permission: analytics:read

Optional: from, to, limit, cursor. enrichmentObservedAt describes the stored enrichment lookup attempt, not the report's age.

Example 200 response

{
  "data": [
    {
      "sourceIp": "192.0.2.10",
      "counts": {
        "messages": 1000,
        "dmarcPassed": 970,
        "dmarcFailed": 20,
        "dmarcUnknown": 10,
        "dispositionNone": 980,
        "dispositionQuarantine": 15,
        "dispositionReject": 5,
        "dispositionUnknown": 0
      },
      "ptr": "mail.example.net",
      "isp": "Example network",
      "country": "ZA",
      "enrichmentObservedAt": "2026-10-02T06:00:00Z"
    }
  ],
  "nextCursor": null
}

List reports

GET /api/v1/domains/{domainId}/reports

Find individual reports and their public IDs and versions. With no date filters, the list includes all retained reports for this domain.

Required permission: reports:read

Optional: from, to, received_from, received_to, limit, cursor. Reports are ordered by original receipt time, then public ID.

Example 200 response

{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000003",
      "domainId": "00000000-0000-4000-8000-000000000002",
      "reporter": "Example Receiver",
      "externalReportId": "sample-report-001",
      "periodStart": "2026-10-01T00:00:00Z",
      "periodEnd": "2026-10-02T00:00:00Z",
      "receivedAt": "2026-10-02T06:00:00Z",
      "modifiedAt": "2026-10-02T06:00:00Z",
      "version": 1,
      "policy": {
        "p": "none",
        "sp": null,
        "np": null,
        "pct": "100",
        "adkim": "r",
        "aspf": "r"
      }
    }
  ],
  "nextCursor": null
}

Read report metadata

GET /api/v1/reports/{reportId}

Read a report's reporting organization, report period, observed policy and version before downloading its records.

Required permission: reports:read

Use the public id returned by the reports endpoint. Report IDs in the browser interface are different.

Example 200 response

{
  "id": "00000000-0000-4000-8000-000000000003",
  "domainId": "00000000-0000-4000-8000-000000000002",
  "reporter": "Example Receiver",
  "externalReportId": "sample-report-001",
  "periodStart": "2026-10-01T00:00:00Z",
  "periodEnd": "2026-10-02T00:00:00Z",
  "receivedAt": "2026-10-02T06:00:00Z",
  "modifiedAt": "2026-10-02T06:00:00Z",
  "version": 1,
  "policy": {
    "p": "none",
    "sp": null,
    "np": null,
    "pct": "100",
    "adkim": "r",
    "aspf": "r"
  }
}

Read report records

GET /api/v1/reports/{reportId}/records

Read groups of messages with their source IP, counts, identifiers, aligned evaluations, raw SPF and DKIM results, and receiver override reasons.

Required permission: reports:read

Optional: limit, cursor, version. Set version to the metadata version to detect a report changing during export. This example shows the first page; its cursor is fictional.

Example 200 response

{
  "reportId": "00000000-0000-4000-8000-000000000003",
  "version": 1,
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000004",
      "sourceIp": "192.0.2.10",
      "count": 970,
      "disposition": "none",
      "alignedDkim": "pass",
      "alignedSpf": "fail",
      "headerFrom": "example.com",
      "envelopeFrom": "bounce.example.com",
      "envelopeTo": null,
      "dkimResults": [
        {
          "domain": "example.com",
          "selector": "mail",
          "result": "pass"
        }
      ],
      "spfResults": [
        {
          "domain": "bounce.example.com",
          "scope": "mfrom",
          "result": "fail"
        }
      ],
      "reasons": []
    }
  ],
  "nextCursor": "opaque-next-page-cursor"
}

Dates and available history

Dates and timestamps use UTC. A range includes its start and excludes its end: from=2026-10-01&to=2026-11-01 covers October. Use UTC midnight boundaries, with from before to.

Optional query parameters
ParameterWhere it appliesHow to use it
from, toSummary, timeseries, sources, reports listReport-period start dates, as YYYY-MM-DD. The default analytics range is the last 30 completed UTC days, shortened to available history and the configured maximum range.
received_from, received_toReports listOriginal receipt timestamps, such as 2026-10-01T12:00:00Z. The start is inclusive and the end exclusive. If both are supplied, the end must be later.
limitDomains, sources, reports list, recordsItems per page: 1 to 500. The default is 100.
cursorDomains, sources, reports list, recordsThe previous response's nextCursor, URL encoded. Keep the original endpoint, key, limit and filters.
versionRecordsThe integer version from report metadata. A mismatch returns 409 report-changed.

The reports list has no default period filter: omitting both from and to lists all retained reports. Supplying either activates period filtering; supply both for a specific historical range. Receipt filters can be combined with period filters.

/access returns availableFrom, the earliest report-period start you can read; null means unlimited plan history. Retention applies even when a report was received recently. An explicit analytics or report-period range outside that history returns 400 retention-window-exceeded.

Read every page

Collection responses contain data and nextCursor. Process the data, then send the returned cursor to read the next page. Stop when nextCursor is null. Records responses also include the report's reportId and version.

  1. Start without a cursor, using the page size and filters you want.
  2. Save nextCursor as NEXT_CURSOR and keep all other query parameters unchanged.
  3. URL encode the cursor and repeat until the response has no next page.
curl --get --fail-with-body \
  -H "Authorization: Bearer $SECUREMARK_API_KEY" \
  --data-urlencode "limit=100" \
  --data-urlencode "cursor=$NEXT_CURSOR" \
  "$SECUREMARK_BASE_URL/api/v1/domains/$DOMAIN_ID/reports"

For a filtered request, repeat its date filters too. Treat the cursor as opaque: do not decode or edit it. Cursors expire after 24 hours; restart from the first page if you receive invalid-cursor. Pagination does not freeze data while reports arrive or change.

Understand the data

DMARC aggregate reports group messages seen by participating receivers. They do not contain message content or cover every message your domain sends. A record's count is the number of messages in that group.

  • dmarcPassed: at least one aligned SPF or DKIM evaluation passed.
  • dmarcFailed: both aligned evaluations failed.
  • dmarcUnknown: neither of those conditions applies.
  • The three DMARC counts add up to messages. The four disposition counts also add up to messages.

In records, alignedDkim and alignedSpf describe alignment evaluations. The separate dkimResults and spfResults arrays contain raw authentication results. A raw pass is not by itself an aligned DMARC pass. Receiver disposition describes how mail was handled and may differ from the published policy.

Daily totals put each report's complete counts on its report-period start day. They are not exact message-delivery timestamps. Reports can overlap or arrive late.

A domain's verified field records ownership verification. A report's externalReportId is the receiver's own identifier; use the public id in API paths. The summary's latestReportReceivedAt covers all retained reports for the domain, even if the chosen date range is narrower.

Policy fields use DMARC tag names. Values such as pct are strings; unavailable policy values can be null.

policyObservedAt and enrichmentObservedAt describe stored observations; null means a timestamp is unavailable. Reading the API does not initiate DNS checks or refresh enrichment. Paused domains remain in domain metadata but return 409 domain-paused for analytics and report reads.

Keep a local copy up to date

  1. List each permitted domain's reports, following every page.
  2. Use the public report id as your local identifier and compare its version with your saved copy.
  3. Fetch all record pages with version set to the metadata version. If you receive report-changed, discard that partial export, fetch fresh metadata, and restart its records.
  4. After the complete report has been read, replace the saved records and counts for that report. Adding the same report again would double-count messages.

Reprocessing preserves the public report ID and original receivedAt, increments version, and updates modifiedAt. Receipt-time polling discovers new arrivals, including reports for older periods; it does not find later edits to reports already received. Regularly reconcile a recent period or scan retained metadata to detect changed versions.

This API has no change feed or deletion notifications. Your integration manages retention of its own saved data.

Errors and retries

API errors return JSON with content type application/problem+json. Use the HTTP status and code to decide what to do. Keep requestId, also returned in the X-Request-Id header, when contacting support. Authentication errors do not redirect to a login page.

{
  "type": "urn:securemark:api:invalid-credential",
  "title": "A valid API key is required.",
  "status": 401,
  "code": "invalid-credential",
  "requestId": "example-request-id"
}
Error responses and recovery
StatusCodeWhat to do
400invalid-requestCheck parameter types, page size and date boundaries.
400invalid-cursorRestart pagination without a cursor, keeping the desired filters.
400retention-window-exceededChoose a range at or after availableFrom.
401invalid-credentialCheck the Bearer header, use the replacement for a rotated key, replace a revoked key, or restore its owner's access.
403insufficient-scope, api-not-enabledCheck the key's read permissions, plan eligibility and organization API switch.
404not-foundCheck the public ID, permitted domains and retained history. Resources outside your access are not disclosed.
409domain-pausedAsk the organization owner or admin to restore the domain's active status.
409report-changedFetch current metadata and restart that report's records.
429rate-limited, queries-busyWait for the number of seconds in Retry-After, then retry with backoff and jitter.
503api-disabledAPI access is currently unavailable. Check availability with support before retrying.
503request-timeout, database-unavailableRetry temporary failures with bounded backoff. For timeouts, reduce the page size or date range.

Retry 429 and temporary 503 failures with a capped number of attempts. Add a small random delay so integrations do not all retry together. Correct authentication, permissions and query errors before retrying them.

For help, contact support with the endpoint, HTTP status and request ID. Never include your API key.

Limits and compatibility

Default limits are 60 requests per minute per key, 300 per organization, 120 per IP address, and two concurrent analytics queries per organization. IPv6 clients share their /64 network's IP limit. Requests across keys in the same organization share its quota.

The default maximum analytics range is 90 days and the maximum page size is 500. Use /access for current per-key and organization request limits, maximum range, page size and available history; a lower plan retention window still applies. Requests have a 30-second deadline.

Responses carry Cache-Control: no-store. Integrations should tolerate additional response fields and unfamiliar result values. Breaking contract changes use a new API version. API v1 covers DMARC aggregate data; raw XML downloads, message content and TLS reports are not exposed.