Scrub validates email addresses via REST. Integrate with your product using an API key — no OAuth flow required.
All endpoints require authentication. Create an API key from your API Keys page and pass it in the header.
X-API-Key: sk_your_key_here
API keys are scoped to your account. All data returned — history, results — belongs exclusively to the key owner.
/v1/validate/single
Validate a single email address. Checks syntax and DNS MX records.
Query parameters
string
requiredEmail address to validate.
Example request
curl -X POST "https://api.thescrub.app/v1/validate/[email protected]" \ -H "X-API-Key: sk_your_key_here"
Response
{
"email": "[email protected]",
"status": "deliverable",
"reason": null,
"quality_score": 95,
"checks": {
"syntax": true,
"mx": true,
"disposable": false,
"role_based": false
},
"attributes": {
"domain": "example.com",
"is_free_provider": false
}
}Status values
deliverable
Syntax valid, MX record found.
undeliverable
Syntax valid but no MX record — email will bounce.
risky
Valid but flagged — disposable domain, role-based address, etc.
invalid_syntax
Failed RFC syntax check.
/v1/validate/bulk
Validate up to 30,000 email addresses in a single request.
Request body
emails
string[]
requiredArray of email addresses. Max 30,000.
response_mode
enum
"all" (default) | "invalid_only" | "summary_only"
dedupe
boolean
Remove duplicates before validating. Default false.
Example request
curl -X POST "https://api.thescrub.app/v1/validate/bulk" \
-H "X-API-Key: sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"emails": ["[email protected]", "[email protected]"],
"response_mode": "all",
"dedupe": true
}'Response
{
"summary": {
"total": 2,
"processed": 2,
"valid": 1,
"invalid": 1,
"errors": 0,
"duplicates_removed": 0,
"duration_ms": 280,
"request_id": "550e8400-e29b-41d4-a716-446655440000"
},
"results": [
{ "email": "[email protected]", "valid": true, "status": "deliverable" },
{ "email": "[email protected]", "valid": false, "status": "undeliverable", "reason": "no_mx" }
]
}The request_id in the summary can be used with GET /v1/history/bulk/{request_id} to retrieve results later.
Submit a batch of emails for asynchronous validation, tagged to one of your integrations. The batch is processed in the background in chunks — poll for progress, or pass event_callback to get pinged as each chunk finishes.
/v1/validate/integration
Submit an email batch for asynchronous processing under an integration.
Request body
integration_id
uuid
requiredThe integration to attribute this batch to. Must belong to you.
batch_id
string
Your own batch identifier, echoed back in the response and every progress event. Server-generated if omitted.
emails
string[]
requiredArray of email addresses. Max 30,000.
event_callback
url
HTTPS URL to receive a progress event after each chunk completes. See Webhooks below for payload shape and signature verification — public URLs only, no private/internal addresses.
Example request
curl -X POST "https://api.thescrub.app/v1/validate/integration" \
-H "X-API-Key: sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"integration_id": "6f1a1c1e-2b7a-4b6a-9c3e-8f2a1d4e5f60",
"emails": ["[email protected]", "[email protected]"],
"event_callback": "https://your-service.com/hook"
}'Response — 202 Accepted
{
"results": {
"request_id": "d6ec6377-70cc-463f-bab4-053dc6e0854d",
"integration_id": "6f1a1c1e-2b7a-4b6a-9c3e-8f2a1d4e5f60",
"batch_id": "1645cf45-c19b-4ded-8a8a-640163aa46f5",
"submitted_count": 1000
}
}Notes
• X-API-Key auth only — session/JWT auth is not accepted on this endpoint, since event_callback deliveries are signed using your API key.
• If your remaining balance is less than the number of emails submitted, the batch is truncated to what's allowed and submitted_count reflects that.
• Track progress with GET /v1/jobs/progress/{request_id}, or rely on event_callback — it fires once per chunk with cumulative counts so far.
All history is scoped to your account. You can only access records created by your own API key or user session.
/v1/history
Cursor-paginated validation history, newest first.
integration_id
uuid
Filter to history tied to a specific integration. Omit to return all.
invalid_only
boolean
Return only invalid results. Default false (returns both).
limit
int
Results per page. Default 50, max 500.
cursor
string
Opaque cursor from a previous page's next_cursor. Omit for the first page.
{
"integration_id": null,
"has_more": false,
"next_cursor": null,
"results": [
{
"request_id": "b1f2...",
"batch_id": null,
"email": "[email protected]",
"status": "invalid",
"error_code": "domain_not_found",
"created_at": "2026-08-11T09:30:00Z"
}
]
}/v1/history/progress
Bulk-poll submitted/validated/invalid counters and status for one or more integration-batch submissions (POST /v1/validate/integration), by the request_id each submission returned.
// Request
{
"integration_id": "11111111-1111-1111-1111-111111111111",
"request_ids": ["b1f2c3d4-...", "c2a3d4e5-..."]
}
// Response
{
"results": [
{
"batch_id": "9f8e7d6c-...",
"submitted_total_count": 100,
"validated_total_count": 85,
"invalid_total_count": 15,
"status": "processing"
}
]
}request_ids that don't exist, or don't belong to this user/integration, are silently omitted from the results rather than erroring.
/v1/history/bulk/{request_id}
All results for a specific bulk job by its request_id.
/v1/history/{email}
All validation history for a specific email address.
/v1/history/{email}
Delete all history for an email address (GDPR right-to-erasure). Returns the number of records deleted.
{ "email": "[email protected]", "deleted": 3 }Manage API keys programmatically. These endpoints require your account session (Bearer token) — not an API key.
/v1/api-keys
Create a new API key. The raw key is returned once and cannot be retrieved again.
name
string
requiredHuman-readable label. 1–100 characters.
{
"id": 1,
"name": "Zapier integration",
"key": "sk_abc123...",
"created_at": "2026-04-15T10:00:00"
}/v1/api-keys
List all your API keys. The raw key is never returned — only metadata.
[
{
"id": 1,
"name": "Zapier integration",
"created_at": "2026-04-15T10:00:00",
"last_used_at": "2026-04-15T12:30:00",
"active": true
}
]/v1/api-keys/{key_id}
Revoke a key immediately. The key stops working as soon as this returns.
Register a URL to receive real-time results after each validation, or pass event_callback on a single request to receive events for just that call — both deliver the same payload shape and are signed the same way, so you can share one verification function.
/v1/webhooks/register
Register a URL to receive validation events.
url
string
requiredHTTPS URL to receive POST payloads.
{
"url": "https://your-service.com/hook",
"secret": "hex-signing-secret",
"message": "Webhook registered. Save the secret — it will not be shown again."
}/v1/webhooks/deregister
Remove a registered webhook by URL.
url
string
requiredThe registered URL to remove (query param).
/v1/webhooks/list
List all registered webhooks.
[
{ "id": 1, "url": "https://...", "active": true, "failure_count": 0 }
]Event payload
Every delivery — registered webhooks and event_callback alike — shares this envelope. data varies by event_type.
{
"event_id": "b3a1c2d4-5e6f-4a1b-8c9d-0e1f2a3b4c5d",
"event_type": "events.progress",
"created_at": "2026-08-13T11:22:37Z",
"integration_id": "6f1a1c1e-2b7a-4b6a-9c3e-8f2a1d4e5f60",
"data": {
"batch_id": "1645cf45-c19b-4ded-8a8a-640163aa46f5",
"status": "completed",
"submitted_total_count": 1000,
"validated_total_count": 940,
"invalid_total_count": 60,
"latest_invalid_emails": ["[email protected]"]
}
}validation.completed
Fired once after /v1/validate/single or /v1/validate/bulk (registered webhooks only). integration_id is null; data holds { endpoint, summary, result? }.
events.progress
Fired once per completed chunk of a POST /v1/validate/integration batch — "processing" for every chunk but the last, "completed" or "failed" for the one that finishes the job.
Verifying payloads
Each request includes an X-Webhook-Signature header shaped t={timestamp},v1={hex_hmac}. For registered webhooks, sign with the secret returned by /v1/webhooks/register. For event_callback, sign with the SHA-256 hash of the API key that made the request — no separate secret to store.
import hmac, hashlib
def verify(payload_bytes: bytes, header: str, secret: str) -> bool:
# header looks like "t=1786360887,v1=9f8a7b..."
parts = dict(p.split("=", 1) for p in header.split(","))
timestamp, signature = parts["t"], parts["v1"]
signed_message = f"{timestamp}.{payload_bytes.decode()}".encode()
expected = hmac.new(secret.encode(), signed_message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
# event_callback secret — no registration step, so it's derived from your API key:
callback_secret = hashlib.sha256(b"sk_your_key_here").hexdigest()Delivery & retries
Respond with any 1xx or 2xx status to acknowledge. Anything else — or a timeout — triggers up to 5 retries with exponential backoff before the delivery is given up on. Registered webhooks additionally get auto-deactivated after 5 consecutive failed deliveries; re-register to reactivate.
© 2026 Scrub - Nerd Zero Private Limited. All rights reserved.