API Authentication
Machine-to-machine requests authenticate with an API key. Dashboard sessions authenticate with a short-lived JWT. Both are accepted on the same endpoints; a key is what you want for integrations and scripts.
Sending your key
X-API-Key: ddmarc_<64 hex characters>
Or as a bearer token, which is handy for clients that only expose an Authorization header:
Authorization: Bearer ddmarc_<64 hex characters>
If both headers are present, X-API-Key takes precedence.
Getting your API key
Key management endpoints
Keys can also be managed over the API. These routes require an admin session or an admin-scoped key.
/api/v1/api-keysList API keys/api/v1/api-keysCreate an API key/api/v1/api-keys/{key_id}Revoke an API keycurl -X POST "https://api.ddmarc.com/api/v1/api-keys" \
-H "X-API-Key: $DDMARC_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "CI pipeline",
"scope": "read",
"expires_in_days": 90
}'{
"id": 7,
"name": "CI pipeline",
"key": "ddmarc_4f1c...b93a",
"key_prefix": "4f1c8ad2",
"scope": "read",
"expires_at": "2026-10-17T09:14:00Z",
"created_at": "2026-07-19T09:14:00Z"
}key is present only on this 201 response. Subsequent GET calls return key_prefix, scope, last_used_at and expires_at only, alongside the plan allowance fields limit, used and can_create.
Making authenticated requests
curl -X GET "https://api.ddmarc.com/api/v1/domains" \ -H "X-API-Key: $DDMARC_API_KEY"
const response = await fetch(
"https://api.ddmarc.com/api/v1/domains",
{ headers: { "X-API-Key": process.env.DDMARC_API_KEY } }
);
const domains = await response.json(); // bare arrayimport os, requests
response = requests.get(
"https://api.ddmarc.com/api/v1/domains",
headers={"X-API-Key": os.environ["DDMARC_API_KEY"]},
)
domains = response.json() # list[dict]API key properties
The literal prefix ddmarc_ followed by 64 lowercase hex characters (32 random bytes) — 71 characters in total. Anything not starting with ddmarc_ is rejected outright.
A key belongs to the organization, not to the person who created it, and always resolves to the same organization context. What it may do is bounded by its scope — not by the creator's role.
Keys do not expire by default. Pass expires_in_days at creation to set one. An expired key stops authenticating immediately.
There is no in-place regenerate. Create a second key, deploy it, then revoke the old one — which means you need a spare slot under your plan's key cap.
ddmarc_4f1c8ad2e07b3915c6a48d2f0b7e9a31c5d84e60f27a1b93c4e5d6f7089a1b2c
Scopes
Scope is chosen when the key is created and cannot be changed afterwards. Scopes are hierarchical: admin satisfies anything requiring write, which satisfies anything requiring read. A request needing more authority than the key carries fails with 403.
| Scope | Level | Grants |
|---|---|---|
read | Default | Read-only access to domains, reports, senders and analytics. |
write | Mutating | Everything in read, plus creating and modifying domains, senders, rules and uploads. |
admin | Full | Everything in write, plus administrative routes such as team management. |
Plan limits
Programmatic API access requires the Protect plan or higher. Requests are metered hourly and pooled across the whole organization, not per key.
429 with Retry-After: 3600. No X-RateLimit-* headers are sent on successful responses, so there is nothing to budget against but your own count.Authentication errors
| Code | detail | Cause |
|---|---|---|
| 401 | Invalid API key | Key is unknown, malformed, expired or revoked. All four produce the same message, so a 401 never confirms that a key ever existed. |
| 401 | Not authenticated | No credential was sent at all. |
| 403 | API key requires 'write' scope or higher | The route needs more authority than the key's scope. |
| 403 | API access requires the Protect plan or higher | Returned when creating a key on a plan without API access. |
| 429 | API rate limit exceeded | Hourly org-wide cap reached. Includes Retry-After: 3600. |
{
"detail": "Invalid API key"
}Errors are always a flat object with a detail field. There is no nested error object and no machine-readable error code — branch on the HTTP status.
JWT sessions
The dashboard and the mobile view authenticate with JWTs, not API keys. You only need this if you are building something that signs a human in.
Security practices
Managing API keys
Every key with its name, prefix, scope, creation date and last-used timestamp, plus how many of your plan's key slots are in use.
Revoking deletes the key record outright. It stops working immediately and cannot be restored — issue a new key instead.
Create, deploy, then revoke. Keep the two overlapping long enough that nothing is mid-request when the old one goes away.