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
- Log in to SpoofWard
- Go to Settings → API Tokens
- Click Create Token
- Name your token (e.g., "Slack Integration" or "Report Export")
- Choose an expiry — 30 days, 90 days (the default) or 1 year. Every token expires; there is no “never” option.
- Select the scopes needed. Start from
readand add the narrowest write scope the integration requires — see Token scopes (abilities) below - Click Generate
- 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
| Ability | Grants |
|---|---|
read | Every 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. |
write | Every mutating (POST / PUT / PATCH / DELETE) endpoint, currently the partner client-management endpoints. Also satisfies every *:write scope. |
Narrower abilities
| Ability | Grants |
|---|---|
domains:read | Read domains and their status |
domains:write | Add, update and remove domains |
rules:write | Manage DMARC rules and sender policies |
members:write | Manage workspace members and invitations |
dns:write | Publish and change DNS records |
exports:read | Download reports and data exports |
webhooks:write | Manage 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:
- A token stored with no abilities at all is unrestricted. Only very old tokens are in this state; recreate them with explicit scopes.
- The wildcard
*satisfies everything. - An exact match — a token holding
domains:writepasses an endpoint that declaresdomains:write. - The broad parent of a narrow ability —
writesatisfies any*:writescope, andreadsatisfies any*:readscope.
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
| Endpoint | Required ability |
|---|---|
GET /api/v1/health | None — public |
GET /api/v1/workspace | read |
GET /api/v1/domains | read |
GET /api/v1/domains/{domain} | read |
GET /api/v1/domains/{domain}/dmarc/summary | read |
GET /api/v1/domains/{domain}/dmarc/reports | read |
GET /api/v1/domains/{domain}/dmarc/top-ips | read |
GET /api/v1/domains/{domain}/dns-health | read |
GET /api/v1/domains/{domain}/dns-health/trend | read |
GET /api/v1/alerts | read |
GET /api/v1/partner, /partner/clients, /partner/clients/{id}, /partner/clients/{id}/domains | read (plus an active MSP workspace whose plan includes API access) |
POST /api/v1/partner/clients | write |
POST /api/v1/partner/clients/{id}/suspend | write |
POST /api/v1/partner/clients/{id}/reactivate | write |
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
| Integration | Grant | Why |
|---|---|---|
| Nightly export of DMARC data to a warehouse | read | Reads only. Never needs a write ability. |
| Scheduled CSV/PDF report download | exports:read | Narrower than read once export endpoints declare it; harmless to grant alongside read today. |
| Provisioning script that adds domains during onboarding | domains:write | Adds and updates domains without also being able to change DNS, members or webhooks. |
| Terraform / CI job that publishes DNS records | dns:write | The most sensitive scope — it changes published DNS. Keep it on its own token with a short expiry. |
| HR joiner/leaver automation | members:write | Manages workspace members and nothing else. |
| Alerting pipeline that manages its own webhook targets | read + webhooks:write | Reads alerts, manages only its own delivery targets. |
| MSP portal automation creating and suspending clients | read + write | The 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 arrivedsender.discovered- New email sender foundsender.authorized- Sender marked as authorizedsender.blocked- Sender marked as blockeddns.changed- DNS record updatedpolicy.updated- DMARC policy changedthreat.detected- Suspicious activity flagged
Setting Up Webhooks
- Settings → Webhooks
- Click Add Webhook
- Enter your webhook URL
- Select events to subscribe to
- 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
- Go to Settings → API Tokens
- Every token in the workspace is listed — not only the ones you created
- 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 - The secret itself is never shown again after creation
Revoke a token
If a token is compromised or no longer needed:
- Find it in the list at Settings → API Tokens
- Click Revoke
- The very next request using it returns
401 Token revoked - 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.
- 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:
- Create a new token with the same (or narrower) scopes
- Deploy it to the integration
- Confirm the new token's last used timestamp is moving
- Confirm the old token's last-used timestamp has stopped moving
- 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:
| Status | Meaning | What to do |
|---|---|---|
200 | Success | — |
401 | No token supplied, the token is unknown, it has expired, or it was revoked | Check the Authorization header; if the token expired or was revoked, create a replacement |
403 | Insufficient scope, or the workspace is inactive | Read the message — it names the exact ability required |
404 | Resource not found, or not part of this workspace | Confirm the ID belongs to the token's workspace |
422 | Validation failed | Inspect the returned field errors |
429 | Rate limited | Honour Retry-After |
500 | Server error | Retry 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
readand add only the narrow write scope the integration actually needs - Prefer
domains:write/dns:write/members:write/webhooks:write/rules:writeover blanketwrite— broadwritesatisfies all of them at once - Give each integration its own token so one revocation does not take everything else down
- Keep
dns:writeisolated 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:
- Check documentation
- Review error message
- Contact support with:
- Error details
- Request method/endpoint
- (Don't share actual token)
Related Documentation
- Organizations - Team and org management
- Email Source Discovery - Sender management via API
- Viewing Reports - Report data via API