Drift Scanner
API reference
HTTP API for Drift Scanner. Register and list environments, trigger scans, diff environments, analyze migrations, and paginate through drift events.
The Drift Scanner API lives at https://drift.arcnull.com. All responses are wrapped in a standard envelope:
{
"success": true,
"data": { /* typed payload */ },
"message": "Human-readable message (optional)",
"errorCode": null,
"timestamp": "2026-04-20T15:10:22.187Z"
}
Base URL
https://drift.arcnull.com
Authentication
Every endpoint requires a JWT issued by the platform auth service. Pass it as a bearer token:
Authorization: Bearer <jwt>
You can mint a JWT by logging in through https://arcnull.com and pulling the token from your dashboard, or by calling the auth API directly (see platform docs — coming soon).
Customer API keys are coming soon. A long-lived API key flow for server-to-server usage is on the roadmap. For now, use a JWT from a logged-in session. When API keys ship, you will be able to mint up to 1 (free) / 5 (pro) / 10+ (growth+) per tenant.
Errors
Errors return success: false with an errorCode and message:
{
"success": false,
"data": null,
"message": "Plan limit reached: your current plan allows up to 2 drift environments. Upgrade your plan to add more.",
"errorCode": "BAD_REQUEST",
"timestamp": "2026-04-20T15:10:22.187Z"
}
Common HTTP statuses:
400— validation / plan-limit / SSRF rejection401— missing or invalid JWT404— resource not found for this tenant429— manual scan rate limit exceeded
Environments
Register an environment
POST /api/v1/drift/environments
Request:
{
"name": "production",
"jdbcUrl": "jdbc:postgresql://prod-db.example.com:5432/myapp",
"username": "arcnull_scanner",
"password": "<strong-password>",
"scanCron": "0 0 * * * *"
}
name— 1-100 chars, must start with a letter or digit, letters/digits/spaces/hyphens/underscoresjdbcUrl— must matchjdbc:postgresql://host/db(?params)?, must not resolve to loopback, private, link-local, metadata, or CGNAT rangesusername— valid PostgreSQL identifier (up to 63 chars)password— up to 512 charsscanCron— optional Spring cron expression (6 fields); defaults to0 0 * * * *(top of every hour)
Response: 201 Created with the new environment.
curl -X POST https://drift.arcnull.com/api/v1/drift/environments \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"name": "production",
"jdbcUrl": "jdbc:postgresql://prod-db.example.com:5432/myapp",
"username": "arcnull_scanner",
"password": "secret",
"scanCron": "0 0 * * * *"
}'
List environments
GET /api/v1/drift/environments
Returns all active environments for the authenticated tenant.
{
"success": true,
"data": [
{
"id": "18a2d7be-4f52-4a86-92b0-52df7d39c7a1",
"name": "production",
"status": "READY",
"scanCron": "0 0 * * * *",
"active": true,
"lastScanned": "2026-04-20T15:00:00Z",
"createdAt": "2026-04-19T09:14:23Z"
}
]
}
Get an environment
GET /api/v1/drift/environments/{id}
Deactivate an environment
DELETE /api/v1/drift/environments/{id}
Sets the environment's active flag to false. History is retained. Returns 200 OK.
Scanning
Trigger a manual scan
POST /api/v1/drift/environments/{id}/scan
Rate-limited per tier (free: 3/min, pro: 10/min). Returns the snapshot that was produced:
{
"success": true,
"data": {
"id": "b7d1a3c4-2a3e-4b7e-9a4f-1c2e9b0d3a77",
"checksum": "sha256:9c3a...",
"tablesCount": 42,
"columnsCount": 317,
"indexesCount": 88,
"capturedAt": "2026-04-20T15:10:22.187Z"
},
"message": "Scan completed"
}
curl -X POST https://drift.arcnull.com/api/v1/drift/environments/$ENV_ID/scan \
-H "Authorization: Bearer $JWT"
Compare two environments
POST /api/v1/drift/compare
Diffs the latest snapshot of one environment against another — useful for "did staging catch everything production ships?"
Request:
{
"baseEnvId": "18a2d7be-4f52-4a86-92b0-52df7d39c7a1",
"headEnvId": "7c1d4e2a-9b3f-4a1c-8d02-3e47f9b8c211"
}
Response:
{
"success": true,
"data": {
"severity": "WARNING",
"breakingCount": 0,
"warningCount": 1,
"infoCount": 3,
"items": [
{
"severity": "WARNING",
"table": "orders",
"column": "shipping_country",
"changeType": "COLUMN_ADDED_NOT_NULL_NO_DEFAULT",
"description": "orders.shipping_country added as NOT NULL without default",
"recommendation": "Backfill and add a default before rolling forward.",
"estimatedImpact": "Inserts from older code paths will fail."
}
]
}
}
Migration analysis
Run a proposed CREATE TABLE / ALTER TABLE / DROP statement through the analyzer to get a pre-flight safety report.
POST /api/v1/drift/migrations/analyze
Request:
{
"sql": "ALTER TABLE users ADD COLUMN email_verified BOOLEAN NOT NULL;",
"environmentId": "18a2d7be-4f52-4a86-92b0-52df7d39c7a1"
}
Response:
{
"success": true,
"data": {
"safe": false,
"overallRisk": "HIGH",
"recommendation": "Add as NULL, backfill, then tighten to NOT NULL.",
"operations": [
{
"sql": "ALTER TABLE users ADD COLUMN email_verified BOOLEAN NOT NULL;",
"operationType": "ADD_COLUMN",
"tableName": "users",
"lockType": "ACCESS EXCLUSIVE",
"estimatedDuration": "~45s",
"tableSize": 2147483648,
"rowCount": 1200000,
"safe": false,
"recommendation": "Adding a NOT NULL column without a default rewrites the table.",
"safeAlternative": "ALTER TABLE users ADD COLUMN email_verified BOOLEAN; /* backfill */; ALTER TABLE users ALTER COLUMN email_verified SET NOT NULL;"
}
]
}
}
Drift events
List events
GET /api/v1/drift/events?page=0&size=20
Paginated. Default size is 20, max 100.
{
"success": true,
"data": {
"content": [
{
"id": "f6c3a3f0-7c5e-4a8c-b4b2-9f3b1f0c4a55",
"envId": "18a2d7be-4f52-4a86-92b0-52df7d39c7a1",
"baselineId": "0ce3b9a2-...",
"currentId": "b7d1a3c4-...",
"severity": "BREAKING",
"breakingCount": 1,
"warningCount": 0,
"infoCount": 2,
"acknowledged": false,
"detectedAt": "2026-04-20T15:10:22.187Z",
"items": [ /* ... */ ]
}
],
"totalElements": 34,
"totalPages": 2,
"number": 0,
"size": 20
}
}
Get a single event
GET /api/v1/drift/events/{id}
Acknowledge an event
POST /api/v1/drift/events/{id}/acknowledge
Marks the event as reviewed. Does not affect the baseline.
Response envelope reference
| Field | Type | Notes |
|---|---|---|
success | boolean | true for 2xx, false otherwise |
data | T | null | Typed payload, null on error |
message | string? | Human-readable hint, useful for surfacing in UIs |
errorCode | string? | Machine-readable code on error (e.g. BAD_REQUEST) |
timestamp | string | ISO-8601 response time, useful for correlating with logs |