REST API for storing, querying, and managing DMARC report and domain data.
- Go 99.8%
- Dockerfile 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .forgejo/workflows | ||
| .github/instructions | ||
| cmd/api | ||
| docs | ||
| internal | ||
| .dockerignore | ||
| .gitignore | ||
| catalog-info.yaml | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| INCIDENT-RUNBOOK.md | ||
| README.md | ||
| renovate.json | ||
| todo.md | ||
service-dmarc-api
A REST API service for storing and querying DMARC aggregate reports. Accepts reports from the DMARC loader and provides query endpoints.
Requirements
- Go 1.25+
- SQLite (included in Go) or PostgreSQL
Configuration
SQLite (default, local development)
go run ./cmd/api
Data stored in dmarc.db in the current directory.
PostgreSQL (production)
export DATABASE_URL="postgres://user:password@localhost/dmarc"
go run ./cmd/api -db-driver postgres
Build
go mod tidy
go build -o api ./cmd/api
./api -addr :8080
Swagger UI
Once the service is running, browse to:
GET /api/docs
The OpenAPI spec is also available at:
GET /api/docs/openapi.json
API Endpoints
Health Check
GET /api/health
Post a Report
POST /api/dmarc/reports
Content-Type: application/json
{
"report_id": "example.com-1234567890",
"reporter_email": "noreply-dmarc-support@example.net",
"org_name": "Example Inc",
"domain": "example.com",
"begin_date": 1234567890,
"end_date": 1234654290,
"policy_p": "reject",
"policy_sp": "reject",
"policy_adkim": "r",
"policy_aspf": "r",
"policy_pct": 100,
"total_messages": 42,
"raw_xml": "<base64-encoded-xml>",
"records": [
{
"source_ip": "192.0.2.1",
"count": 10,
"policy_disp": "none",
"dkim_result": "pass",
"spf_result": "pass",
"header_from": "example.com",
"envelope_from": "noreply@example.com",
"envelope_to": "user@example.com"
}
]
}
Note: duplicate detection is based on the pair (reporter_email, report_id).
Legacy alias: POST /api/reports
Response:
{
"id": 1
}
Duplicate response:
HTTP 409 Conflict
{
"error": "duplicate report",
"code": "REPORT_DUPLICATE",
"report_id": "example.com-1234567890",
"reporter_email": "noreply-dmarc-support@example.net"
}
List Reports
GET /api/dmarc/reports?domain=example.com&limit=10&start_date=2026-01-01&end_date=2026-01-31
Legacy alias: GET /api/reports
Query parameters (optional):
domain- Filter by domainlimit- Max results (default 100)after_date- Legacy RFC3339 timestamp, treated asstart_datewhenstart_dateis not setstart_date- Start bound forbegin_date(YYYY-MM-DDor RFC3339)end_date- End bound forbegin_date(YYYY-MM-DDor RFC3339)
Response:
{
"total": 2,
"reports": [
{
"id": 1,
"report_id": "example.com-1234567890",
"org_name": "Example Inc",
"domain": "example.com",
"begin_date": "2026-01-01T00:00:00Z",
"end_date": "2026-01-02T00:00:00Z",
"policy_p": "reject",
...
}
]
}
Get Report with Records
GET /api/dmarc/reports/{id}
Legacy alias: GET /api/reports/{id}
Response:
{
"report": {
"id": 1,
"report_id": "example.com-1234567890",
...
},
"records": [
{
"id": 1,
"report_id": 1,
"source_ip": "192.0.2.1",
"count": 10,
...
}
]
}
Get Domain DMARC Aggregate Records
GET /api/dmarc/domains/{domain}/records?limit=1000&start_date=2026-01-01&end_date=2026-01-31
Legacy alias: GET /api/domains/{domain}/records
Project Structure
service-dmarc-api/
cmd/api/
main.go # CLI entry point
internal/
db/
db.go # Database setup and migrations
store.go # Database queries
models/
report.go # Data models
server/
server.go # HTTP handlers
go.mod
README.md
Observability Conventions
Logs
- All request failures and write audits are emitted as structured logs.
- Denied access events are logged as
access_deniedwith fields:action,result,reason,statusrequest_id,method,path,actor,actor_type
- DB write audit events are logged as
db_change_audit.
Metrics
- HTTP server metrics:
dmarc_api_http_requests_totaldmarc_api_http_request_duration_seconds
- Operation metrics (write paths):
dmarc_api_operations_total{operation,result,error_class}dmarc_api_operation_duration_seconds{operation}
- Auth, verification, and workflow metrics:
dmarc_api_auth_failures_total{guard,reason}dmarc_api_domain_verify_attempts_total{forced,result}dmarc_api_domain_verify_dns_lookup_failures_totaldmarc_api_defaults_seeded_total{record_type,source}dmarc_api_instance_register_total{service,result}
Tracing
- Request-level instrumentation is enabled via OpenTelemetry HTTP middleware.
- Write operations create additional spans named by route operation keys.
- Outbound HTTP requests propagate trace context and
X-Request-Id.
Request ID and Trace Propagation
- Incoming request IDs are read from
REQUEST_ID_HEADERS(default includesX-Request-Id). - If no inbound request ID exists, a UUID is generated and returned as
X-Request-Id. - Outbound HTTP requests inject:
- W3C trace context via OTel propagator
X-Request-Idfrom request context when available