Features Pricing
Free Tools
DMARC Checker SPF Checker DKIM Checker Domain Scanner Blacklist Checker Header Analyzer All 11 tools →
Resources
What is DMARC? What is SPF? What is DKIM? What is BIMI? Academy Blog Docs MSP Program Contact
Sign in Start →

API Tokens

SpoofWard provides a comprehensive REST API for automating DMARC management, integrating with your tools, and building custom workflows. This guide explains how to create and manage API tokens.

What is the SpoofWard API?

The API allows you to programmatically:

  • Retrieve DMARC reports - Integrate report data into your systems
  • Manage domains - Add, update, and delete domains
  • Manage senders - Authorize, block, or investigate senders
  • Update DNS records - Configure SPF, DKIM, DMARC programmatically
  • Trigger scans - Initiate domain scans on demand
  • Export data - Bulk export reports and analytics
  • Manage team members - Add/remove users programmatically
  • Create webhooks - Listen for real-time events

Getting Started

Step 1: Create an API Token

  1. Log in to SpoofWard
  2. Go to Settings → API Tokens
  3. Click Create Token
  4. Name your token (e.g., "Slack Integration" or "Report Export")
  5. Choose an expiry — 30 days, 90 days (the default) or 1 year. Every token expires; there is no “never” option.
  6. Select the scopes needed. Start from read and add the narrowest write scope the integration requires — see Token scopes (abilities) below
  7. Click Generate
  8. Complete the two-factor challenge and confirm your password when prompted — both are required for this action

You must be an owner or admin of the workspace, on a plan that includes API access, and within your plan's token limit.

Step 2: Copy Your Token

Important: Copy the token immediately. You won't see it again.


ms_1a2b3c4d5e6f7g8h9i0j...

Tokens begin with ms_. Only the first 11 characters are ever stored in readable form (as the token's prefix, used to identify it in the UI and in logs); the rest is stored as a SHA-256 hash and cannot be recovered.

Store it securely:

  • Never commit to Git
  • Never share in Slack/email
  • Store in environment variable or secrets manager
  • Only share with trusted integrations

Step 3: Use in API Calls

Include token in request header:


curl -H "Authorization: Bearer ms_1a2b3c4d5e6f7g8h9i0j" \
  https://spoofward.com/api/v1/domains

Or in your code:


import requests

headers = {
    "Authorization": "Bearer ms_1a2b3c4d5e6f7g8h9i0j"
}

response = requests.get(
    "https://spoofward.com/api/v1/domains",
    headers=headers
)

Token scopes (abilities)

A token carries one or more abilities, also called scopes. There are two broad abilities and seven narrower ones. At least one is required — a token is never created with an empty scope set, and the form never pre-selects Write.

Broad abilities

AbilityGrants
readEvery GET endpoint: workspace info, domains, DMARC summaries / reports / top IPs, DNS health and its trend, alerts, and the partner (MSP) read endpoints. Also satisfies every *:read scope.
writeEvery mutating (POST / PUT / PATCH / DELETE) endpoint, currently the partner client-management endpoints. Also satisfies every *:write scope.

Narrower abilities

AbilityGrants
domains:readRead domains and their status
domains:writeAdd, update and remove domains
rules:writeManage DMARC rules and sender policies
members:writeManage workspace members and invitations
dns:writePublish and change DNS records
exports:readDownload reports and data exports
webhooks:writeManage webhooks and alert channels

How a scope is resolved

When a request reaches an endpoint, SpoofWard checks the endpoint's declared ability against the token, in this order:

  1. A token stored with no abilities at all is unrestricted. Only very old tokens are in this state; recreate them with explicit scopes.
  2. The wildcard * satisfies everything.
  3. An exact match — a token holding domains:write passes an endpoint that declares domains:write.
  4. The broad parent of a narrow ability — write satisfies any *:write scope, and read satisfies any *:read scope.

The implication that matters most: broad write satisfies every narrow write scope, but a narrow scope never satisfies a broad one. A token holding only domains:write is still refused by an endpoint that declares plain write. Granting write is therefore strictly wider than granting every narrow write scope individually — prefer the narrow ones.

Which ability each endpoint needs

EndpointRequired ability
GET /api/v1/healthNone — public
GET /api/v1/workspaceread
GET /api/v1/domainsread
GET /api/v1/domains/{domain}read
GET /api/v1/domains/{domain}/dmarc/summaryread
GET /api/v1/domains/{domain}/dmarc/reportsread
GET /api/v1/domains/{domain}/dmarc/top-ipsread
GET /api/v1/domains/{domain}/dns-healthread
GET /api/v1/domains/{domain}/dns-health/trendread
GET /api/v1/alertsread
GET /api/v1/partner, /partner/clients, /partner/clients/{id}, /partner/clients/{id}/domainsread (plus an active MSP workspace whose plan includes API access)
POST /api/v1/partner/clientswrite
POST /api/v1/partner/clients/{id}/suspendwrite
POST /api/v1/partner/clients/{id}/reactivatewrite

The narrow abilities (domains:write, rules:write, members:write, dns:write, exports:read, webhooks:write) are accepted and stored today and are what forthcoming write endpoints will declare. Issuing them now is safe and future-proof: a token holding domains:write will work on the domain write endpoints the moment they ship, without your having to grant blanket write.

Least-privilege examples

IntegrationGrantWhy
Nightly export of DMARC data to a warehousereadReads only. Never needs a write ability.
Scheduled CSV/PDF report downloadexports:readNarrower than read once export endpoints declare it; harmless to grant alongside read today.
Provisioning script that adds domains during onboardingdomains:writeAdds and updates domains without also being able to change DNS, members or webhooks.
Terraform / CI job that publishes DNS recordsdns:writeThe most sensitive scope — it changes published DNS. Keep it on its own token with a short expiry.
HR joiner/leaver automationmembers:writeManages workspace members and nothing else.
Alerting pipeline that manages its own webhook targetsread + webhooks:writeReads alerts, manages only its own delivery targets.
MSP portal automation creating and suspending clientsread + writeThe partner endpoints declare the broad abilities, so these are required today.

Rule of thumb: grant the narrowest ability that makes the integration work, and give each integration its own token. One token per integration means one revocation, not a shared outage.

Creating a token: what is required

Token creation is deliberately gated. To create one you must:

  • Hold the owner or admin role in the workspace. Members and viewers cannot create or revoke tokens.
  • Be on a plan that includes API access. Without it the form refuses with an upgrade message.
  • Be within your plan's API token limit. Revoke an unused token or upgrade to add more.
  • Have two-factor authentication enabled and satisfied for the session.
  • Confirm your password — a fresh password prompt appears even though you are already signed in.
  • Stay within the creation rate limit of 10 requests per minute on the token-management endpoints.

Expiry

Every token gets a finite expiry chosen at creation: 30 days, 90 days (the default) or 1 year (the maximum). Tokens that never expire are no longer issued, and a token created through the API without an expires_at gets 90 days. An expired token is rejected with 401 Token expired — it is not silently renewed, and expiry cannot be extended on an existing token. Create a replacement and revoke the old one.

Metadata recorded per token

The API Tokens page shows, for every token:

  • Name — whatever you called it at creation.
  • Prefix — the first 11 characters of the raw token (ms_ plus 8 random characters). This is non-secret and exists so you can identify a token in the UI and in logs. The token itself is stored only as a SHA-256 hash and can never be shown again.
  • Abilities — the exact scopes granted.
  • Created by and created at.
  • Last used at and last used IP — updated on every authenticated request. A token with no last-used timestamp has never been used; a token last used from an unexpected address should be revoked immediately.
  • Expires at.

Token creation and revocation are both written to the workspace audit log, including the list of abilities granted.

Common Use Cases

Use Case 1: Export Reports

Export DMARC reports to your data warehouse:


import requests
import json

api_token = "ms_1a2b3c4d5e6f7g8h9i0j"
domain = "example.com"

headers = {"Authorization": f"Bearer {api_token}"}

# Get reports for last 30 days
response = requests.get(
    f"https://spoofward.com/api/v1/domains/{domain}/reports",
    headers=headers,
    params={"days": 30}
)

reports = response.json()

# Save to file
with open("dmarc_reports.json", "w") as f:
    json.dump(reports, f)

Use Case 2: Slack Notifications

Get alerts when senders change:


import os

from flask import Flask, request
import requests

app = Flask(__name__)

@app.route("/webhook/senders", methods=["POST"])
def sender_webhook():
    data = request.json

    if data["event"] == "sender_discovered":
        sender = data["sender"]["name"]
        volume = data["sender"]["volume"]

        # Post to Slack
        slack_message = f"New sender discovered: {sender} ({volume} messages)"
        requests.post(
            os.environ["SLACK_WEBHOOK"],
            json={"text": slack_message}
        )

    return "OK", 200

Use Case 3: Auto-Authorize Known Services

Approve known services automatically:


import requests

api_token = "ms_1a2b3c4d5e6f7g8h9i0j"
domain = "example.com"

known_services = [
    "SendGrid",
    "Mailchimp",
    "Office 365"
]

headers = {"Authorization": f"Bearer {api_token}"}

# Get all senders
response = requests.get(
    f"https://spoofward.com/api/v1/domains/{domain}/senders",
    headers=headers
)

senders = response.json()

# Authorize known ones
for sender in senders:
    if sender["name"] in known_services:
        requests.post(
            f"https://spoofward.com/api/v1/domains/{domain}/senders/{sender['id']}/authorize",
            headers=headers
        )

Use Case 4: Compliance Reporting

Generate compliance reports automatically:


import requests
from datetime import datetime, timedelta

api_token = "ms_1a2b3c4d5e6f7g8h9i0j"

headers = {"Authorization": f"Bearer {api_token}"}

# Get all domains
domains_response = requests.get(
    "https://spoofward.com/api/v1/domains",
    headers=headers
)

domains = domains_response.json()

# Generate report for each domain
report = {
    "date": datetime.now().isoformat(),
    "domains": []
}

for domain in domains:
    # Get current score
    score_response = requests.get(
        f"https://spoofward.com/api/v1/domains/{domain['id']}/score",
        headers=headers
    )

    score = score_response.json()

    report["domains"].append({
        "name": domain["name"],
        "dmarc_score": score["dmarc_score"],
        "policy": score["policy"],
        "pass_rate": score["pass_rate"]
    })

print(json.dumps(report, indent=2))

API Endpoints

Note: the authoritative list of endpoints that exist today — and the ability each one requires — is the endpoint table under Token scopes (abilities) above. Some examples in this section illustrate planned write endpoints; they are the reason the narrow write scopes exist, and a token issued with those scopes now will work against them without needing broad write.

Domains

List domains:


GET /v1/domains

Get domain details:


GET /v1/domains/{domain_id}

Add domain:


POST /v1/domains
{
  "name": "example.com",
  "description": "Main domain"
}

Update domain:


PATCH /v1/domains/{domain_id}
{
  "description": "Updated description"
}

Delete domain:


DELETE /v1/domains/{domain_id}

Reports

List reports:


GET /v1/domains/{domain_id}/reports?days=30

Get specific report:


GET /v1/domains/{domain_id}/reports/{report_id}

Export reports:


GET /v1/domains/{domain_id}/reports/export?format=csv&days=30

Senders

List senders:


GET /v1/domains/{domain_id}/senders

Get sender details:


GET /v1/domains/{domain_id}/senders/{sender_id}

Authorize sender:


POST /v1/domains/{domain_id}/senders/{sender_id}/authorize

Block sender:


POST /v1/domains/{domain_id}/senders/{sender_id}/block

Update sender:


PATCH /v1/domains/{domain_id}/senders/{sender_id}
{
  "notes": "Our marketing platform",
  "status": "authorized"
}

DNS Records

Get DNS records:


GET /v1/domains/{domain_id}/dns-records

Update DNS record (Hosted DNS only):


PATCH /v1/domains/{domain_id}/dns-records/{record_id}
{
  "value": "v=spf1 include:sendgrid.net ~all"
}

Team Members

List team members:


GET /v1/organization/team

Invite member:


POST /v1/organization/team/invite
{
  "email": "[email protected]",
  "role": "analyst"
}

Remove member:


DELETE /v1/organization/team/{member_id}

Webhooks

Webhooks deliver real-time events to your system.

Available Events

  • report.received - New DMARC report arrived
  • sender.discovered - New email sender found
  • sender.authorized - Sender marked as authorized
  • sender.blocked - Sender marked as blocked
  • dns.changed - DNS record updated
  • policy.updated - DMARC policy changed
  • threat.detected - Suspicious activity flagged

Setting Up Webhooks

  1. Settings → Webhooks
  2. Click Add Webhook
  3. Enter your webhook URL
  4. Select events to subscribe to
  5. Click Create

SpoofWard will POST to your URL when events occur.

Webhook Payload

Example payload when report arrives:


{
  "event": "report.received",
  "timestamp": "2024-01-15T10:30:00Z",
  "domain_id": "d_123abc",
  "domain_name": "example.com",
  "data": {
    "report_id": "r_456def",
    "period": "2024-01-14",
    "messages": 5000,
    "pass_rate": 0.95,
    "policy": "none"
  }
}

Verifying Webhook Signature

Webhooks include an X-SpoofWard-Signature header for verification:


import hmac
import hashlib

def verify_webhook(payload, signature, secret):
    expected = hmac.new(
        secret.encode(),
        payload.encode(),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected)

Managing Tokens

List your tokens

  1. Go to Settings → API Tokens
  2. Every token in the workspace is listed — not only the ones you created
  3. Each row shows the non-secret prefix (ms_ plus 8 characters), the exact scopes granted, who created it, when it was created, when it expires, and when and from which IP address it was last used
  4. The secret itself is never shown again after creation

Revoke a token

If a token is compromised or no longer needed:

  1. Find it in the list at Settings → API Tokens
  2. Click Revoke
  3. The very next request using it returns 401 Token revoked
  4. The token stays in the list, greyed out and marked Revoked with the date and who revoked it — the record of what once had access is part of your audit trail. Delete record on a revoked row removes it permanently.
  5. Revocation cannot be undone — generate a new token if you still need one

Revoking requires the owner or admin role, and the action is recorded in the workspace audit log.

Rotation

There is no in-place rotation: a token's secret, scopes and expiry are fixed at creation. Rotate by overlap instead:

  1. Create a new token with the same (or narrower) scopes
  2. Deploy it to the integration
  3. Confirm the new token's last used timestamp is moving
  4. Confirm the old token's last-used timestamp has stopped moving
  5. Revoke the old token

The default 90-day expiry makes this a quarterly habit. Rotate immediately if a token leaks, if it was last used from an address you do not recognise, or when someone who could see it leaves the team.

Rate limits

Rate limits are per token and apply to every plan alike:

  • Per token (/api/v1/*) — 120 requests per minute
  • Per workspace (all tokens in the workspace combined) — 600 requests per minute
  • Per route — a further 60 requests per minute, keyed by source address
  • Token management (creating, listing and revoking tokens) — 10 requests per minute

Whichever ceiling is reached first applies, so one busy integration cannot spend the whole workspace's budget.

Laravel's standard throttling headers are returned on every response:


X-RateLimit-Limit: 60
X-RateLimit-Remaining: 42
Retry-After: 17

Exceeding the limit returns 429 Too Many Requests. Back off for the number of seconds in Retry-After rather than retrying immediately.

Error handling

The API returns standard HTTP status codes:

StatusMeaningWhat to do
200Success
401No token supplied, the token is unknown, it has expired, or it was revokedCheck the Authorization header; if the token expired or was revoked, create a replacement
403Insufficient scope, or the workspace is inactiveRead the message — it names the exact ability required
404Resource not found, or not part of this workspaceConfirm the ID belongs to the token's workspace
422Validation failedInspect the returned field errors
429Rate limitedHonour Retry-After
500Server errorRetry with backoff; contact support if it persists

Missing token


HTTP/1.1 401 Unauthorized
{
  "error": "Unauthenticated",
  "message": "API token required. Provide via Authorization: Bearer <token> header."
}

Unknown or expired token


HTTP/1.1 401 Unauthorized
{ "error": "Invalid token" }

HTTP/1.1 401 Unauthorized
{ "error": "Token expired" }

Insufficient scope

This is the response you get when the token is valid but does not carry the ability the endpoint declares. The message names both the exact scope and the broad ability that would also satisfy it, so you can decide which to grant:


HTTP/1.1 403 Forbidden
{
  "error": "Forbidden",
  "message": "Token does not have the 'domains:write' ability (or the broader 'write')."
}

For an endpoint declaring a broad ability there is no narrower alternative, so the message says so plainly:


HTTP/1.1 403 Forbidden
{
  "error": "Forbidden",
  "message": "Token does not have 'write' ability."
}

A 403 is never retried into success. Issue a new token with the required scope — scopes cannot be added to an existing token.

Inactive workspace


HTTP/1.1 403 Forbidden
{ "error": "Workspace inactive or not found" }

Security Best Practices

Store tokens securely:

  • Use environment variables
  • Use secrets manager (AWS Secrets Manager, HashiCorp Vault)
  • Never commit to code

Use minimal scopes:

  • Start from read and add only the narrow write scope the integration actually needs
  • Prefer domains:write / dns:write / members:write / webhooks:write / rules:write over blanket write — broad write satisfies all of them at once
  • Give each integration its own token so one revocation does not take everything else down
  • Keep dns:write isolated on its own short-lived token — it can change published DNS
  • Review the last-used timestamp and IP regularly, and revoke anything unused

Rotate tokens regularly:

  • Yearly or after changes
  • Immediately if exposed
  • Keep old token revoked

Monitor usage:

  • Check "last used" timestamp
  • Remove old unused tokens
  • Track which apps use which tokens

Limit distribution:

  • Don't share tokens between apps
  • Create separate token per integration
  • Easier to track and revoke

Documentation

Full API documentation available at:

https://docs.spoofward.com/api/

Includes:

  • Detailed endpoint reference
  • Code examples (Python, JavaScript, Go, etc.)
  • Authentication details
  • Rate limiting info
  • Error codes
  • SDK libraries

Support

For API issues:

  1. Check documentation
  2. Review error message
  3. Contact support with:
  • Error details
  • Request method/endpoint
  • (Don't share actual token)

Related Documentation

Your domain is being tested right now.
Are you watching?

Protect your brand and improve deliverability — automatically, with continuous monitoring and alerts.