Endpoints // Contact
Check whether an email address can receive mail and get a verdict (valid, risky, invalid or unknown) by polling or webhook.
Credits
1 credit per verdict, whatever it says.
A check that fails is refunded.
Introduction
The Verify Email endpoint checks whether an email address can receive mail, and returns one verdict: valid, risky, invalid or unknown. Processing is asynchronous: the API acknowledges the request with a webhookId, then you retrieve the verdict by polling (recommended, no public endpoint required) or through a webhook callback (advanced, push delivery).
Use this endpoint to clean a list before a send, or to check an address that comes from a form, a CRM or another source. Addresses returned by Find Email are already verified, so you do not need to check them again.
This endpoint costs 1 credit per verdict, whatever the verdict says, unknown included. A check that fails without a verdict is refunded. See Verdicts.
Authorization
| Name | Type | Required | Description |
|---|---|---|---|
apikey | string | Yes | Your API key from the developer dashboard. Pass it in the Authorization: Bearer YOUR_API_KEY header. |
Request body
| Name | Type | Required | Description |
|---|---|---|---|
email | string | Yes | The email address to check, such as jane@acme.com. |
webhookUrl | string | Optional | HTTPS URL that receives the result, including error outcomes. Omit it to poll for the verdict instead. |
A request with a missing or malformed email is refused with a 422 and costs nothing: no job starts.
Idempotency-Key header
A check is debited when its job is created. If the response gets lost (a timeout, a dropped connection), sending the request again would start and bill a second check. To retry safely, send an Idempotency-Key header with a key you choose for this check, and send the same key again when you retry.
| Name | Type | Required | Description |
|---|---|---|---|
Idempotency-Key | string | Optional | A key you choose for this check: 1 to 255 printable ASCII characters without spaces, such as a UUID. Use a new key for each new check. |
For 24 hours, a request with the same key and the same body returns the response of the first one, unchanged, with the same webhookId and the header Idempotent-Replayed: true. No new check starts and no credits are debited: the replayed quotas describe the first request. Keys are scoped to your workspace and to this endpoint. A replayed request still counts toward your per-minute rate limit.
| Status | Code | When |
|---|---|---|
409 | IDEMPOTENCY_KEY_IN_USE | The first request with this key is still being processed. Retry after the Retry-After seconds to receive its response. |
422 | IDEMPOTENCY_KEY_REUSED | The key was already used with a different body. Use a new key for a new check. |
400 | INVALID_IDEMPOTENCY_KEY | The key is empty, longer than 255 characters, or contains a space or a non-ASCII character. |
A first request that ends in an error is not kept: it started nothing and cost nothing, so a retry with the same key runs as a new request.
Idempotency-Key
curl -X POST https://api.reversecontact.com/v2/contact/email/verify \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 4f1c2a9e-3b7d-4c55-9a8e-1d2f3a4b5c6d" \
-d '{"email":"jane@acme.com"}'import ReverseContact from "@reverse-contact/sdk";
const client = new ReverseContact({ apiKey: process.env.RC_API_KEY });
// The SDK sends a new Idempotency-Key on each call and reuses it on its own retries.
// To retry after a restart, store your own key with the request and pass it again.
const job = await client.contact.verifyEmail(
{ email: "jane@acme.com" },
{ idempotencyKey: "4f1c2a9e-3b7d-4c55-9a8e-1d2f3a4b5c6d" }
);
console.log(job.id, job.replayed); // replayed: true when the first response came backimport os
from reversecontact import ReverseContact
with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client:
# The SDK sends a new Idempotency-Key on each call and reuses it on its own retries.
# To retry after a restart, store your own key with the request and pass it again.
job = client.contact.verify_email(
email="jane@acme.com",
idempotency_key="4f1c2a9e-3b7d-4c55-9a8e-1d2f3a4b5c6d",
)
print(job.id, job.replayed) # replayed: True when the first response came backVerdicts
The verdict is the status field of the result. It is not the status of the job: when you poll, data.status is the job (succeeded) and data.result.status is the verdict.
| Verdict | Meaning | What to do |
|---|---|---|
valid | The address can receive mail. | Send. |
risky | The address may receive mail, but delivery is not guaranteed. A catch-all domain is a common cause. | Send with care, or keep it out of a cold send. |
invalid | The address cannot receive mail. | Do not send. |
unknown | The check was inconclusive. | Treat it as unverified. You can check it again later. |
Every verdict costs 1 credit.
Response structure
This endpoint replies immediately with a webhookId. You then retrieve the result in one of two ways:
- Polling (recommended): call
GET /v2/webhooks/:webhookIduntil the status issucceeded. With?wait=20, each call waits up to 20 seconds and answers as soon as the result is ready. No webhook URL is needed. See the Polling guide. - Webhook callback (advanced): if you provided a
webhookUrl(or a workspace default is configured), Reverse Contact POSTs the verdict to that URL once the check completes. See Webhook callback payload (advanced) below.
| Field | Type | Description |
|---|---|---|
success | boolean | true if the job was created successfully |
dataobjectInitial job acknowledgement. Save the webhookId to retrieve the result via polling (GET /v2/webhooks/:webhookId) or to correlate an upcoming webhook callback (null on error responses).
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
status | string | Job status, always "created" for the initial response |
webhookId | string | Unique identifier (UUID). Save it to poll the result at GET /v2/webhooks/:webhookId, or to correlate an incoming webhook callback. |
pollUrl | string | Convenience hint pointing to the polling endpoint for this job (e.g. /v2/webhooks/{webhookId}). |
| Field | Type | Description |
|---|---|---|
error | null | Always null on success |
quotasobjectCredits and rate limit usage after this request
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
creditsConsumed | number | Credits debited when the job is created: 1 |
workspaceobjectWorkspace-level limits
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
id | string | Your workspace identifier |
hasUnlimitedCredits | boolean | Whether workspace credits are unlimited |
allowDailyOvercost | boolean | Whether the workspace may exceed its daily cap |
creditsobjectCredit balance
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
total | number | Total credits in your plan |
used | number | Credits consumed so far |
left | number | Remaining credits |
dailyLimitobjectDaily request limit (null if not configured)
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
limit | number | Maximum requests per day |
used | number | Requests made today |
left | number | Remaining requests today |
nextReset | string | When the daily window resets (ISO 8601) |
minuteRateLimitobjectPer-minute rate limit
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
limit | number | Maximum requests per minute |
used | number | Requests made in the current minute |
left | number | Remaining requests in the current minute |
nextReset | string | When the minute window resets (ISO 8601) |
keyobjectAPI key-level limits (null when request is from dashboard)
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
id | string | API key identifier |
dailyLimitobjectDaily limit for this key (null if not configured)
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
limit | number | Maximum requests per day |
used | number | Requests made today |
left | number | Remaining requests today |
nextReset | string | When the daily window resets (ISO 8601) |
minuteRateLimitobjectMinute rate limit for this key
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
limit | number | Maximum requests per minute |
used | number | Requests made in the current minute |
left | number | Remaining requests in the current minute |
nextReset | string | When the minute window resets (ISO 8601) |
metadataobjectRequest tracking information
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
requestId | string | Unique identifier for this request |
executionTimeMs | number | Total execution time in milliseconds |
Webhook callback payload (advanced)
When the check returns a verdict, Reverse Contact sends a JSON POST request to your webhookUrl. The callback is not wrapped in the V2 response envelope. It always includes the webhookId from the initial acknowledgement and a data value.
Success payload
{
"webhookId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f",
"data": {
"email": "jane@acme.com",
"status": "valid"
}
}dataobjectThe verdict for the address you sent.
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
email | string | The address that was checked |
status | string | The verdict: valid, risky, invalid or unknown. See Verdicts. |
Webhook callback errors (advanced)
When the check cannot return a verdict, data holds an errorCode that gives the reason:
Error payload
{
"webhookId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f",
"data": {
"errorCode": "fetch-data-error"
}
}data.errorCode takes one of these values:
| Code | Billed | Description |
|---|---|---|
invalid-request | No | The address has a form the check cannot read |
fetch-data-error | No | The check failed before a verdict; safe to retry |
webhook-url-invalid | Yes | The callback URL returned a 4xx response |
webhook-url-errored | Yes | The callback URL returned a 5xx response or a network error |
webhook-url-timeout | Yes | The callback URL did not respond in time |
webhook-url-unreachable | Yes | The callback URL could not be reached |
Code examples
Code Examples
import ReverseContact, { JobFailedError } from "@reverse-contact/sdk";
const client = new ReverseContact({ apiKey: process.env.RC_API_KEY });
const job = await client.contact.verifyEmail({ email: "jane@acme.com" });
console.log("Save this job id:", job.id); // resume after a restart with client.jobs.wait(id)
try {
// Long polls until the job ends, 30 minutes at most by default.
const done = await job.wait();
console.log(done.data.result?.email, done.data.result?.status);
} catch (error) {
if (error instanceof JobFailedError) console.log("Ended without a verdict:", error.errorCode, "credits:", error.creditsConsumed);
else throw error;
}import ReverseContact, { parseWebhookDelivery } from "@reverse-contact/sdk";
const client = new ReverseContact({ apiKey: process.env.RC_API_KEY });
const job = await client.contact.verifyEmail({
email: "jane@acme.com",
webhookUrl: "https://your-app.com/webhooks/reversecontact"
});
console.log(job.status, job.id);
// The route behind webhookUrl receives { webhookId, data }: answer within 10 seconds.
async function handleWebhook(request: Request): Promise<Response> {
const delivery = parseWebhookDelivery(await request.json());
console.log(delivery.webhookId, delivery.isError ? "failed" : "succeeded", delivery.data);
return new Response(null, { status: 200 });
}KEY="YOUR_API_KEY"
ID=$(curl -sX POST https://api.reversecontact.com/v2/contact/email/verify \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"email":"jane@acme.com"}' | jq -r .data.webhookId)
while :; do
S=$(curl -s https://api.reversecontact.com/v2/webhooks/$ID \
-H "Authorization: Bearer $KEY" | jq -r .data.status)
[ "$S" = "succeeded" ] && break
[ "$S" = "errored" ] && echo "job failed" && exit 1
sleep 2
done
curl -s https://api.reversecontact.com/v2/webhooks/$ID \
-H "Authorization: Bearer $KEY" | jq .data.resultcurl -X POST https://api.reversecontact.com/v2/contact/email/verify \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "jane@acme.com",
"webhookUrl": "https://your-app.com/webhooks/reversecontact"
}'import os
from reversecontact import JobFailedError, ReverseContact
with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client:
job = client.contact.verify_email(email="jane@acme.com")
print("Save this job id:", job.id) # resume after a restart with client.jobs.wait(id)
try:
# Long polls until the job ends, 30 minutes at most by default.
done = job.wait()
if done.data and done.data.result:
print(done.data.result.email, done.data.result.status)
except JobFailedError as error:
print("Ended without a verdict:", error.error_code)import os
from reversecontact import ReverseContact, parse_webhook_delivery
with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client:
job = client.contact.verify_email(
email="jane@acme.com",
webhook_url="https://your-app.com/webhooks/reversecontact",
)
print(job.status, job.id)
# The route behind webhook_url receives {webhookId, data}: answer within 10 seconds.
def handle_webhook(body: bytes) -> None:
delivery = parse_webhook_delivery(body)
print(delivery.webhook_id, "failed" if delivery.is_error else "succeeded", delivery.data)Example response
Example Response
{
"success": true,
"data": {
"status": "created",
"webhookId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f",
"pollUrl": "/v2/webhooks/c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f"
},
"error": null,
"metadata": {
"requestId": "req_v1e2r3i4f5y6",
"executionTimeMs": 95
},
"quotas": {
"creditsConsumed": 1,
"workspace": {
"id": "ws_abc123",
"credits": {
"total": 10000,
"used": 4537,
"left": 5463
},
"dailyLimit": null,
"minuteRateLimit": {
"limit": 60,
"used": 10,
"left": 50,
"nextReset": "2025-01-20T14:16:00.000Z"
},
"hasUnlimitedCredits": false,
"allowDailyOvercost": false
},
"key": {
"id": "key_abc123",
"dailyLimit": null,
"minuteRateLimit": {
"limit": 60,
"used": 10,
"left": 50,
"nextReset": "2025-01-20T14:16:00.000Z"
}
}
}
}{
"success": true,
"data": {
"webhookId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f",
"status": "succeeded",
"eventType": "FETCH_CONTACT_DATA",
"errorCode": null,
"createdAt": "2026-11-03T12:43:02.017Z",
"result": {
"email": "jane@acme.com",
"status": "valid"
},
"creditsConsumed": 1
}
}{
"webhookId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f",
"data": {
"email": "jane@acme.com",
"status": "valid"
}
}