API reference
Base URL https://app.emailshield.co/api. Every request needs the header X-API-Key: evf_…. Request bodies are JSON (Content-Type: application/json).
Verify
Section titled “Verify”POST /v1/verify/single
Section titled “POST /v1/verify/single”Verifies one address and returns the answer in the response. Waits up to 30 seconds; if there’s no answer by then you get 408 and the credit is refunded. Costs 1 credit.
Body
| Field | Type | |
|---|---|---|
email | string | Required. The address to verify. |
check_smtp | boolean | Default true. Ask the mail server. Leave it on. |
Response 200
| Field | Type | |
|---|---|---|
email | string | |
status | string | valid, invalid, or risky (catch-all or disposable) |
valid | boolean | |
score | number | Confidence, 0 to 1 |
reason | string | Plain-English reason, e.g. Mailbox confirmed |
details | object | See Reading a result |
credits_used | integer | 1 |
credits_remaining | integer | Plan credits left (see note below) |
processed_at | string | ISO 8601 time |
POST /v1/verify/bulk
Section titled “POST /v1/verify/bulk”Submits a list as a background job and returns 202 straight away. Costs 1 credit per unique address, charged now; Unknown results are refunded when the job finishes.
Body
| Field | Type | |
|---|---|---|
emails | string[] | Required. 1 to 50,000 addresses. Duplicates (ignoring case) are removed before charging. Every item must be a syntactically valid address, or the whole call fails with 422. |
name | string | Optional. The list name shown in the dashboard (up to 255 characters). |
webhook_url | string | Optional. HTTPS URL we POST to when the job finishes. See Webhooks. |
priority | string | normal (default), high or urgent. |
check_smtp | boolean | Default true. |
Response 202
{ "job_id": "3f1c2a4e-…", "status": "queued", "total_emails": 2, "estimated_seconds": 0, "credits_used": 2, "credits_remaining": 48997, "created_at": "2026-10-09T12:01:00Z"}estimated_seconds is a rough guess; don’t build timeouts on it.
GET /v1/jobs/{job_id}
Section titled “GET /v1/jobs/{job_id}”Job progress and totals. This is the cheap endpoint to poll.
| Field | Type | |
|---|---|---|
job_id | string | |
status | string | queued, processing, completed, failed, interrupted or cancelled |
terminal | boolean | true when the job has stopped. Stop polling when this is true. |
total_emails | integer | |
processed | integer | Addresses with a final answer so far |
progress_percent | number | |
verdicts | object | Counts by verdict: valid, invalid, catch_all, risky, role_based, disposable, unknown |
buckets | object | Counts by group: safe_to_send, risky, invalid |
created_at, started_at, completed_at | string | ISO 8601 times |
error | string | null | Set when the job failed |
valid_count and invalid_count are also returned for older integrations; use verdicts and buckets instead.
GET /v1/jobs/{job_id}/results
Section titled “GET /v1/jobs/{job_id}/results”The answers, page by page. Rows appear while the job runs — only final answers are stored — so you can start reading before it finishes.
| Query | ||
|---|---|---|
page | integer | Default 1 |
per_page | integer | Default 100, maximum 1000 |
{ "job_id": "3f1c2a4e-…", "status": "completed", "total": 2, "page": 1, "per_page": 100, "has_more": false, "results": [ { "email": "jane.doe@example.com", "status": "valid", "valid": true, "score": 0.95, "reason": "Mailbox confirmed", "details": {"primary_label": "valid", "tags": [], "label_reasons": ["Mailbox confirmed"]} }, { "email": "info@example.org", "status": "invalid", "valid": false, "score": 0.5, "reason": "Domain accepts all addresses", "details": { "primary_label": "catch_all", "tags": [{"label": "role", "confidence": 0.8}], "label_reasons": ["Domain accepts all addresses", "Role-based address"], "safe_to_send": "risky", "safe_to_send_score": 22 } } ]}GET /v1/jobs
Section titled “GET /v1/jobs”Your recent jobs, newest first.
| Query | ||
|---|---|---|
status | string | Optional filter, e.g. completed |
limit | integer | Default 20, maximum 100 |
offset | integer | Default 0 |
Returns {"jobs": [{"job_id", "status", "total_emails", "processed", "progress_percent", "created_at"}], "limit", "offset"}.
Reading a result
Section titled “Reading a result”The verdict lives in details:
| Field | |
|---|---|
primary_label | valid, invalid, catch_all, inbox_full, disabled, unknown or not_verified |
tags | List of {"label", "confidence"} with label one of spam_trap, disposable, role |
label_reasons | Plain-English reasons, e.g. Mailbox confirmed, Domain has no mail server |
safe_to_send, safe_to_send_score, safe_to_send_reasons | Catch-all rows only: the send-risk rating |
details can carry other fields too; treat anything not listed here as informational and subject to change.
To match the dashboard’s categories, apply tags first, then the label:
| If… | Dashboard category | Group |
|---|---|---|
tags contain spam_trap | Spam Trap | Invalid |
tags contain disposable | Disposable | Invalid |
tags contain role | Role | Risky |
primary_label is valid | Valid | Safe to send |
primary_label is catch_all | Catch-All | Risky |
primary_label is inbox_full | Inbox Full | Risky |
primary_label is invalid or disabled | Invalid / Disabled / No Mail Server | Invalid |
primary_label is unknown or not_verified | Unknown / Not Verified (refunded) | Risky |
Account
Section titled “Account”GET /v1/account/credits
Section titled “GET /v1/account/credits”credits, credits_used_today (last 24 hours), credits_used_this_month (last 30 days), renewal_date.
GET /v1/account
Section titled “GET /v1/account”plan, credits, credits_used_today and a few account fields.
GET /v1/subscription
Section titled “GET /v1/subscription”status, plan, credits, renewal_date, has_billing, is_trial, trial_expired.
DELETE /v1/keys/{key_id}
Section titled “DELETE /v1/keys/{key_id}”Revokes a key. Returns 204, or 404 if there’s no such key. Keys are created in the app under Profile → API.