Developers
SenderGuard exposes two read-only endpoints—no inbox access, no unsubscribe callbacks, EML discarded after parsing. Include a descriptive User-Agent (e.g. acme-infra-monitor/1.1) so we can reach you if something looks off.
Rate limits
- Anonymous (per IP): 5 requests / minute
- API key (
x-api-keyor Bearer): 30 requests / minute
Every 429 includes Retry-After (seconds) and X-RateLimit-Remaining.
OpenAPI
Download YAMLCopy the schema below or pull it directly from /openapi.yaml.
openapi: 3.1.0
info:
title: SenderGuard Public API
version: "1.0.0"
description: |
Read-only endpoints for retrieving SenderGuard audit summaries and verification proof.
Anonymous access is limited to 5 requests per minute per IP. Authenticated access with
an API key allows up to 30 requests per minute. Include a descriptive User-Agent string
with contact information whenever possible.
servers:
- url: https://senderguard.com
description: Production
paths:
/api/audit:
get:
summary: Fetch the latest audit for a domain
description: Returns the newest stored audit (score, badges, identifiers) for the requested domain.
parameters:
- name: domain
in: query
description: Fully qualified domain name to lookup.
required: true
schema:
type: string
example: example.com
responses:
'200':
description: Audit summary for the domain
content:
application/json:
schema:
$ref: '#/components/schemas/AuditResponse'
'400':
description: Invalid domain supplied
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'404':
description: No audit found for the domain yet
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'429':
description: Rate limit exceeded
headers:
Retry-After:
description: Seconds to wait before retrying.
schema:
type: integer
X-RateLimit-Remaining:
description: Remaining quota in the current window.
schema:
type: integer
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/api/verify:
get:
summary: Verify a stored scan proof
description: Replays the stored SenderGuard scan proof and returns verification metadata for assessment.
parameters:
- name: scanId
in: query
description: Identifier returned by the audit endpoint or dashboard.
required: true
schema:
type: string
example: d3f4cedcab12abcd
responses:
'200':
description: Verification bundle for the supplied scanId
content:
application/json:
schema:
$ref: '#/components/schemas/VerifyResponse'
'404':
description: Scan proof not found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/api/admin/rulepack:
post:
summary: Upload or update a RulePack (admin)
description: |
Creates or updates a RulePack by version. Requires an admin session email to match `ADMIN_EMAILS`,
or an `x-admin-email` header when `ALLOW_ADMIN_HEADER=1` is set. Rollout percent defaults to 100.
parameters:
- name: x-admin-email
in: header
required: false
description: Admin email (header override enabled only when ALLOW_ADMIN_HEADER=1).
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [version, json]
properties:
version:
type: string
example: v1
json:
description: RulePack JSON payload used by the scorer
type: object
rolloutPercent:
type: integer
minimum: 0
maximum: 100
example: 100
responses:
'200':
description: RulePack stored
content:
application/json:
schema:
type: object
properties:
ok:
type: boolean
example: true
rulepack:
$ref: '#/components/schemas/RulePack'
'401':
description: Unauthorized
'403':
description: Forbidden (admin only)
'429':
description: Rate limit exceeded
headers:
Retry-After:
schema:
type: integer
X-RateLimit-Remaining:
schema:
type: integer
/api/admin/rulepack/activate:
post:
summary: Activate a RulePack version (admin)
description: Activates the specified RulePack and optionally sets a rollout percent (bucketed by seed).
parameters:
- name: x-admin-email
in: header
required: false
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [version]
properties:
version:
type: string
example: v2
rolloutPercent:
type: integer
minimum: 0
maximum: 100
example: 20
responses:
'200':
description: Activated RulePack
content:
application/json:
schema:
type: object
properties:
ok:
type: boolean
rulepack:
$ref: '#/components/schemas/RulePack'
'401': { description: Unauthorized }
'403': { description: Forbidden (admin only) }
'404': { description: RulePack not found }
/api/admin/rulepack/rollback:
post:
summary: Roll back to a previous RulePack (admin)
description: |
Rolls back to a specific version or to the previous snapshot when `to=prev` is supplied.
parameters:
- name: x-admin-email
in: header
required: false
schema:
type: string
- name: to
in: query
required: false
schema:
type: string
example: prev
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
toVersion:
type: string
example: v1
responses:
'200':
description: Rolled back successfully
content:
application/json:
schema:
type: object
properties:
ok:
type: boolean
rulepack:
$ref: '#/components/schemas/RulePack'
'401': { description: Unauthorized }
'403': { description: Forbidden (admin only) }
'404': { description: RulePack not found }
/api/admin/rulepack/current:
get:
summary: Fetch current RulePack snapshot (admin)
description: Returns the active RulePack, control (previous), and recent history. Supports ETag.
parameters:
- name: x-admin-email
in: header
required: false
schema:
type: string
- name: If-None-Match
in: header
required: false
schema:
type: string
responses:
'200':
description: Current snapshot
headers:
ETag:
description: Snapshot hash for conditional requests.
schema:
type: string
Cache-Control:
schema:
type: string
example: no-store
content:
application/json:
schema:
type: object
properties:
version: { type: string, nullable: true }
rolloutPercent: { type: integer }
active: { $ref: '#/components/schemas/RulePack' }
control: { $ref: '#/components/schemas/RulePack' }
history:
type: array
items:
$ref: '#/components/schemas/RulePack'
etag: { type: string }
'304': { description: Not Modified }
'401': { description: Unauthorized }
'403': { description: Forbidden (admin only) }
'429':
description: Rate limit exceeded
headers:
Retry-After:
description: Seconds to wait before retrying.
schema:
type: integer
X-RateLimit-Remaining:
description: Remaining quota in the current window.
schema:
type: integer
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
components:
schemas:
AuditResponse:
type: object
properties:
domain:
type: string
description: Domain that was audited.
example: example.com
score:
type: integer
description: Composite score across SPF, DKIM, DMARC, alignment, and unsubscribe checks.
example: 94
badges:
type: array
description: Pass/fail state for the five compliance badges.
items:
$ref: '#/components/schemas/Badge'
scanId:
type: string
description: Identifier for verifying or exporting the audit.
example: d3f4cedcab12abcd
sha256:
type: string
description: Canonical hash of the audit payload.
example: 8c53afbaf7ff853bd323df8b7861c12d732a94751b3254a5d6d6b1b6492a6bbe
rulepackVersion:
type: string
description: Active rule pack version when the audit ran.
example: demo
suggestions:
type: array
description: Top recommendations captured with the audit.
items:
type: string
required:
- score
- badges
- scanId
- sha256
VerifyResponse:
type: object
properties:
domain:
type: string
description: Domain associated with the scan proof.
matched:
type: boolean
nullable: true
description: Whether the recomputed hash matches the stored baseline (null when baseline unavailable).
scanId:
type: string
example: d3f4cedcab12abcd
sha256:
type: string
description: Recomputed hash of the stored result.
rulepackVersion:
type: string
description: Rule pack version recorded with the scan.
diffs:
type: array
description: Material differences recorded since the previous scan.
items:
type: string
resolvers:
type: array
description: DNS resolvers used when recomputing the proof.
items:
type: string
required:
- scanId
- sha256
- diffs
- resolvers
Badge:
type: object
properties:
label:
type: string
example: SPF
status:
type: string
enum: [pass, warn]
example: pass
ErrorResponse:
type: object
properties:
error:
type: string
example: rate_limited
retryAfter:
type: integer
description: Only present when the request hit a rate limit.
RulePack:
type: object
properties:
version:
type: string
active:
type: boolean
rolloutPercent:
type: integer
createdAt:
type: string
format: date-time
activatedAt:
type: string
format: date-time
nullable: true
summary:
type: string
nullable: true
json:
type: object
description: Included on some endpoints to echo stored rulepack payload.Examples with Retry-After handling
curl
Loops on HTTP 429 and sleeps for the advertised seconds.
#!/usr/bin/env bash
set -euo pipefail
API="https://senderguard.com/api/audit?domain=example.com"
UA="acme-monitor/1.1"
KEY="${SG_API_KEY:-}"
for attempt in {1..5}; do
response=$(mktemp)
body=$(mktemp)
curl -sS -D "$response" -o "$body" \
-H "User-Agent: $UA" \
$([ -n "$KEY" ] && echo -H "x-api-key: $KEY") \
"$API"
status=$(awk 'NR==1 {print $2}' "$response")
if [ "$status" != "429" ]; then
cat "$body" && rm "$response" "$body"
exit 0
fi
retry=$(awk 'tolower($1)=="retry-after:" {print $2}' "$response")
sleep "${retry:-10}"
done
echo 'Gave up after 5 attempts' >&2
exit 1fetch (Node)
import fetch from "node-fetch";
async function getAudit(domain, apiKey) {
const url = new URL("https://senderguard.com/api/audit");
url.searchParams.set("domain", domain);
for (let attempt = 0; attempt < 5; attempt += 1) {
const res = await fetch(url, {
headers: {
"User-Agent": "acme-monitor/1.1",
...(apiKey ? { "x-api-key": apiKey } : {}),
},
});
if (res.status !== 429) {
if (!res.ok) throw new Error(`Failed: ${res.status}`);
return res.json();
}
const retry = Number(res.headers.get("retry-after")) || 10;
await new Promise((resolve) => setTimeout(resolve, retry * 1000));
}
throw new Error("Rate limit persisted after retries");
}
getAudit("example.com", process.env.SG_API_KEY).then(console.log);Python requests
import os
import time
import requests
API = "https://senderguard.com/api/verify"
HEADERS = {"User-Agent": "acme-monitor/1.1"}
if os.getenv("SG_API_KEY"):
HEADERS["x-api-key"] = os.environ["SG_API_KEY"]
params = {"scanId": "d3f4cedcab12abcd"}
for _ in range(5):
resp = requests.get(API, headers=HEADERS, params=params, timeout=30)
if resp.status_code != 429:
resp.raise_for_status()
print(resp.json())
break
retry = int(resp.headers.get("Retry-After", "10"))
time.sleep(retry)
else:
raise RuntimeError("Rate limit persisted after retries")Next steps
Need higher bursts or webhook delivery? Generate an API key inside Settings, wire the response into your monitoring, and keep verifying hashes with /scan/<id>#why-it-passed.
Read-only · No email bodies · EML discarded after parsing