Build and resell your own email verification service
Build your own paid email verification service on the EmailPal API: your auth and billing in front, one server-side key behind. Blocks of 500 checks a day are $9.95/month, about $0.0007 per check at full use.
The short answer
You can build a paid email verification service on the EmailPal API: put your own authentication and billing in front, call POST /api/public/v1/verify and POST /api/public/v1/lists/verify with one server-side key, and sell the results as your own product. EmailPal sells capacity, not checks. A block of 500 checks a day costs $9.95/month, which is 15,000 checks a month, or about $0.0007 per check at full utilisation, plus a base plan from $69/month.
Terms section 9 allows embedding list verification results in your own product through the REST API under your own account and name. The cost is a subscription (Starter $69/mo includes 500 checks a day) plus blocks of 500 a day at $9.95/month, up to 200 blocks. The $0.0007 figure only holds if every check is used. You must enforce your own customer quotas, handle 429 and 503 with Retry-After, and not promise more than you have bought.
How it works, step by step
Step 1
Subscribe and size your daily capacity
Verification needs an active subscription. Starter at $69/mo includes 500 checks a day, and each extra block of 500 a day is $9.95/month, up to 200 blocks. Size the number of blocks to your busiest expected day, not your average, because the allowance is daily and does not carry over.
Step 2
Create one write-scope key and keep it on your server
Create the key on the API keys page. Verification needs write scope. Never give the key to your customers: Terms section 10 forbids sharing API keys between organisations, so each customer’s access has to run through your own keys and your own API.
Step 3
Put your own authentication and billing in front
Your customers authenticate to you, with keys you issue and plans you price. EmailPal has no concept of your end customers, no per-customer quota and no per-check billing, so metering and invoicing are yours to build.
Step 4
Enforce your own daily quota before calling EmailPal
Reserve each customer’s checks against your own counter first. Keep the sum of your customers’ daily limits at or below what you have purchased, and release a reservation when EmailPal answers 429 or 503.
Step 5
Call /verify for single checks and /lists/verify for files
Single addresses go to POST /verify synchronously. Files go to POST /lists/verify, which answers 202 and streams results as they are decided. Use the list endpoint for volume, because your hourly request limit is 1,000 on every plan.
Step 6
Handle 429 and 503 with a queue
On 503 capacity_exhausted wait for the Retry-After header and retry, because nothing was counted. On 429 verification_limit_reached queue the work until midnight UTC. On 429 rate_limited wait for Retry-After.
Step 7
Map the eight statuses to your own schema
Return your own verdict names and keep EmailPal’s status in a reason field. The mapping below groups safe as deliverable, catch_all, role_account and disposable as risky, invalid, disabled and inbox_full as undeliverable, and unknown as unknown.
01
The architecture
Your customer calls your API. Your service authenticates them, checks their plan and quota, and puts the work on a queue. A worker in your service calls EmailPal with a single server-side key, receives the result, and returns it under your schema. All of the customer-facing surface is yours: the domain, the documentation, the keys, the dashboard and the invoice.
EmailPal sits behind that as one account. It sees one API key and one allowance, never your customers. That is why your service has to be the meter. EmailPal’s credits counter is a single daily number for the whole account, and it will not tell you which customer used which check.
Your customer
-> YOUR API (your keys, your plans, your rate limits, your invoice)
-> YOUR QUEUE (daily quota per customer, retries, backoff)
-> EmailPal API (one write-scope key, one daily allowance)
POST /api/public/v1/verify single address, synchronous
POST /api/public/v1/lists/verify file, 202, poll for results02
What you are buying: capacity, not checks
EmailPal sells verification as a daily allowance. Starter ($69/mo) includes 500 checks a day, Growth ($189/mo) 1,500 and Scale ($499/mo) 4,500. Extra capacity comes in blocks of 500 a day at $9.95/month each, on any plan, up to 200 blocks. A subscription is required: without one every call returns 402 subscription_required, and there is no verification-only product.
Every address costs one credit, whether it is checked singly or inside a list, and the allowance is shared between the two endpoints. The credit is taken before the probe. It is spent for results that need no connection (role_account, disposable, syntax errors) and for a single check that comes back unknown. The 503 is the exception: that check is not counted. Lists are different: the credits for the whole file are reserved when it is accepted, and the API reference says they are refunded for addresses that finish unknown.
The ceiling is real. Blocks stop at 200, which is 100,000 extra checks a day, and a purchase can be refused with 409 capacity_unavailable when EmailPal has no unsold verification capacity left. EmailPal checks each increase against the capacity of its verification IP pool. Terms section 9 says capacity above what you have bought is not guaranteed and requests above it may be refused with a capacity error.
03
Honest unit economics
The marginal price of capacity is $9.95 for one block of 500 checks a day. Over a 30 day month that is 15,000 checks, so $9.95 divided by 15,000 is $0.000663, or about $0.0007 per check and $0.66 per 1,000. This is the price of capacity, and it only equals the price of a check when every check in the block is used, every day.
It is also not the whole bill. Capacity needs a base plan, and the cheapest is Starter at $69/mo, which already includes 500 a day. Spread across the checks you can actually run, the all-in cost falls as you add blocks but never reaches the block price. At 50% utilisation every figure doubles, and checks that return unknown, role_account, disposable or invalid cost the same as a safe one.
The arithmetic below assumes a 30 day month, 100% utilisation, Starter as the base, and excludes payment processing, your hosting, support and any bad debt. The base plan also includes mailboxes, a sequencer and a CRM that a pure verification reseller may never use, so it is a fixed cost to you. On Scale ($499/mo) with 200 blocks the all-in figure is $2,489 for 3,135,000 checks, or $0.00079, so Starter is the cheaper base unless you need the rest of Scale.
One block alone
500/day x 30 days = 15,000 checks/month
$9.95 / 15,000 = $0.000663 per check (about $0.66 per 1,000)
at 50% utilisation = $9.95 / 7,500 = $0.001327 (about $1.33 per 1,000)
Starter base ($69/mo, includes 500/day) plus blocks
0 blocks $69.00 500/day 15,000/mo $0.0046 per check
10 blocks $168.50 5,500/day 165,000/mo $0.00102 per check
20 blocks $268.00 10,500/day 315,000/mo $0.00085 per check
100 blocks $1,064.00 50,500/day 1,515,000/mo $0.00070 per check
200 blocks $2,059.00 100,500/day 3,015,000/mo $0.00068 per check (maximum)04
Daily caps and your own quotas
The allowance resets at midnight UTC and does not carry over. A customer who runs a 400,000 address list on Monday needs 400,000 credits available on Monday, because the list endpoint reserves them all at once and refuses a file that does not fit. The 429 for that case carries addresses, used and limit, so you can split the file.
Our recommendation for what you sell: set each customer a daily limit, and keep the total of those limits at or below the daily cap you bought. Overselling that total is how a busy customer starves the rest. Every success response includes credits.used and credits.limit, so mirror them as a cross-check against your own counter.
Your own request volume also matters. Every call, including each poll of a list, counts toward the 1,000 requests an hour on your plan. A reseller serving single-address checks from many customers can hit that long before the daily allowance, so batch work into lists.
05
Handling 429, 503 and 402
503 capacity_exhausted means every verification IP was at its hourly limit. The check was not counted. The wait in seconds is in the Retry-After header and in error.retryAfter. Retry after that long, and tell your own customer the check is queued rather than failed.
429 verification_limit_reached means your daily allowance is spent. The response has no Retry-After header, so the wait is the time until midnight UTC. Buying another block raises the cap. 429 rate_limited is the hourly request limit and does carry a Retry-After header. Tell them apart by error.code, not by the status.
402 subscription_required or verification_capacity_required is a billing state on your account, not a transient error. Do not retry it. Alert yourself, and return your own error to customers. Any other 5xx means the worker did not answer, and it is safe to retry, but only the 503 states that the credit was returned.
06
Mapping the eight statuses to your own schema
The API returns eight statuses and a sub_status. A reseller will usually collapse them into three or four public verdicts and keep the raw status as a reason, so customers who care can see it. The grouping below follows how EmailPal’s own list gate treats them: catch_all, role_account and disposable are the risky bucket, and invalid, disabled and inbox_full are the bounce bucket.
Keep inbox_full separate from invalid in the raw reason. It is a bounce today but the mailbox exists and may recover. Never map unknown to a verdict a customer might read as good or bad. It means no definitive answer, usually greylisting or a timeout, and the sub_status (antispam_system, unable_to_connect, no_mx_record) says which.
07
A minimal TypeScript wrapper
Node 18+ with the built-in fetch. This is the shape of the integration, not production code: it has no persistence, and the quota functions are yours to implement.
const BASE_URL = 'https://www.emailpal.io/api/public/v1';
type Status =
| 'safe' | 'catch_all' | 'role_account' | 'disposable'
| 'inbox_full' | 'disabled' | 'invalid' | 'unknown';
interface VerifyResponse {
email: string;
status: Status;
sub_status: string | null;
domain: string;
mx_host: string | null;
checked_at: string;
credits: { used: number; limit: number };
}
interface ListAccepted {
upload_id: string;
addresses: number;
job_id: string;
poll: string;
results: string;
credits: { used: number; limit: number };
}
class EmailPalError extends Error {
constructor(
readonly httpStatus: number,
readonly code: string,
message: string,
readonly retryAfterSeconds: number | null,
) {
super(message);
}
}
async function call<T>(method: 'GET' | 'POST', path: string, body?: unknown): Promise<T> {
const res = await fetch(BASE_URL + path, {
method,
headers: {
Authorization: 'Bearer ' + process.env.EMAILPAL_API_KEY, // ep_live_..., write scope
'Content-Type': 'application/json',
},
body: body === undefined ? undefined : JSON.stringify(body),
signal: AbortSignal.timeout(50_000), // the worker budget is 45s, the route allows 60s
});
if (res.ok) return (await res.json()) as T;
const payload = await res.json().catch(() => null);
const err = payload?.error ?? {};
const header = res.headers.get('retry-after');
const retryAfter = header ? Number(header) : typeof err.retryAfter === 'number' ? err.retryAfter : null;
throw new EmailPalError(res.status, err.code ?? 'unknown_error', err.message ?? res.statusText, retryAfter);
}
export function secondsUntilUtcMidnight(now = new Date()): number {
const next = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1);
return Math.max(1, Math.ceil((next - now.getTime()) / 1000));
}
export type Outcome =
| { kind: 'result'; result: VerifyResponse }
| { kind: 'retry_later'; seconds: number; reason: string }
| { kind: 'failed'; code: string; message: string };
// One attempt. Scheduling the retry is the queue's job, not this function's.
export async function verifyOnce(email: string): Promise<Outcome> {
try {
const result = await call<VerifyResponse>('POST', '/verify', { email });
return { kind: 'result', result };
} catch (e) {
if (!(e instanceof EmailPalError)) {
return { kind: 'retry_later', seconds: 30, reason: 'network_or_timeout' };
}
if (e.code === 'verification_limit_reached') {
return { kind: 'retry_later', seconds: secondsUntilUtcMidnight(), reason: e.code };
}
if (e.code === 'capacity_exhausted' || e.code === 'rate_limited') {
return { kind: 'retry_later', seconds: e.retryAfterSeconds ?? 60, reason: e.code };
}
if (e.httpStatus >= 500) {
return { kind: 'retry_later', seconds: 30, reason: e.code };
}
return { kind: 'failed', code: e.code, message: e.message }; // 400, 401, 402, 403: do not retry
}
}
export function submitList(content: string, filename?: string) {
return call<ListAccepted>('POST', '/lists/verify', { content, filename });
}
export function getListResults(uploadId: string, cursor?: string) {
const query = new URLSearchParams({ limit: '200' });
if (cursor) query.set('cursor', cursor);
return call<{
data: Array<{ email: string; status: Status; sub_status: string | null; domain: string; checked_at: string }>;
has_more: boolean;
next_cursor: string | null;
list: { id: string; status: string; finished: boolean; total: number };
}>('GET', '/lists/' + uploadId + '/results?' + query.toString());
}
export type PublicVerdict = 'deliverable' | 'risky' | 'undeliverable' | 'unknown';
export function toPublicVerdict(status: Status): PublicVerdict {
switch (status) {
case 'safe':
return 'deliverable';
case 'catch_all':
case 'role_account':
case 'disposable':
return 'risky';
case 'invalid':
case 'disabled':
case 'inbox_full':
return 'undeliverable';
default:
return 'unknown';
}
}08
Your handler in front of it
The shape of your own endpoint. Reserve against your quota first, call EmailPal, release the reservation whenever EmailPal did not answer, and return your own schema. The reservation and authentication functions are yours.
import { verifyOnce, toPublicVerdict } from './emailpal';
export async function POST(request: Request) {
const customer = await authenticateCustomer(request); // your auth
const { email } = await request.json();
if (!(await reserveQuota(customer.id, 1))) { // your daily meter
return Response.json({ error: 'quota_exceeded' }, { status: 429 });
}
const outcome = await verifyOnce(email);
if (outcome.kind === 'retry_later') {
await releaseQuota(customer.id, 1); // nothing was answered
return Response.json(
{ error: 'temporarily_unavailable', retry_after: outcome.seconds },
{ status: 503, headers: { 'Retry-After': String(outcome.seconds) } },
);
}
if (outcome.kind === 'failed') {
await releaseQuota(customer.id, 1);
return Response.json({ error: 'verification_failed' }, { status: 502 });
}
const { result } = outcome;
await recordUsage(customer.id, result); // your invoice line
return Response.json({
email: result.email,
verdict: toPublicVerdict(result.status), // your schema
reason: result.status,
detail: result.sub_status,
});
}
declare function authenticateCustomer(request: Request): Promise<{ id: string }>;
declare function reserveQuota(customerId: string, count: number): Promise<boolean>;
declare function releaseQuota(customerId: string, count: number): Promise<void>;
declare function recordUsage(customerId: string, result: unknown): Promise<void>;09
What the Terms say
This is a summary of section 9 of the Terms of Service, which govern. You may embed list verification results, and mailbox and domain provisioning, in your own product or service and supply them to your own end customers, provided you do so through the REST API or MCP server, under your own account and your own name.
You are responsible for your end users. You must make sure each of them complies with the acceptable use policy and the Terms, and anything an end user does through your account is treated as your act. API keys must not be shared between organisations, so each end customer’s access has to run through your keys and not through keys you hand to them (section 10).
Verification allowances are daily and capped at the amount you purchased. EmailPal does not guarantee capacity beyond what you have purchased, and requests above it may be refused with a capacity error, so do not promise your own customers more than you have bought. What still needs a separate written agreement is reselling access to the dashboard, white-labelling the Service, or using the EmailPal name and brand as your own.
Two practical notes follow from this. Your own customers’ lists and addresses will pass through EmailPal, so your own privacy terms need to cover that, and EmailPal keeps a per-check audit row for instant checks. Per-address list results are kept for 90 days. Ask a lawyer to read the Terms against your business before you launch. This page is not legal advice.
Limits, prices and names, in one table
The figures this page relies on, so you do not have to hunt for them.
| Cost of one block | $9.95/month500 checks a day. |
| Checks per block | 15,000 per 30 days |
| Marginal cost per check | $0.000663About $0.0007, or $0.66 per 1,000. Only at full utilisation. |
| At 50% utilisation | $0.001327About $1.33 per 1,000. |
| Cheapest base plan | Starter, $69/moIncludes 500 checks a day and 50 mailboxes. |
| Starter plus 10 blocks | $168.50 for 165,000/mo$0.00102 per check at full use. |
| Starter plus 200 blocks (maximum) | $2,059 for 3,015,000/mo$0.00068 per check at full use. |
| Maximum purchasable | 200 blocks100,000 checks a day on top of the plan allowance. Purchases can also be refused with 409 capacity_unavailable. |
| Hourly API request limit | 1,000On Starter, Growth and Scale. Each poll counts. |
| Allowance resets | 00:00 UTCDaily, and does not carry over. |
| Scope required | write |
| Billing model | Monthly subscriptionNo per-check metered billing and no prepaid credits. |
Checked against the product and the API on . Plans and limits change, so confirm in the docs before you build on them.
This is not for you if
- You need more than the capacity you can buy: 200 blocks a day on top of your plan allowance, and purchases can be refused when EmailPal has no capacity left to sell.
- You want EmailPal to meter and invoice per check. There is no per-check billing, no prepaid credit and no per-customer breakdown. A flat daily allowance is all it offers.
- You want a hosted, white-labelled dashboard for your customers. Reselling the dashboard or using the EmailPal brand needs a written agreement, and there is no white-label interface.
- You need guaranteed capacity or an uptime commitment you can pass to your customers. A 503 is possible, and Terms section 9 says capacity beyond what you purchased is not guaranteed.
- You want to give each of your customers their own EmailPal key. Keys cannot be shared between organisations, so every customer must go through your API.
- Your volume is low. If you check a few hundred addresses a day for yourself, you do not need any of this: Starter already includes 500 a day.
Common questions
Can I resell EmailPal email verification?
Yes, through the API. Terms section 9 lets you embed list verification results in your own product and supply them to your own end customers through the REST API or MCP server, under your own account and your own name. Reselling the dashboard, white-labelling, or using the EmailPal brand as your own needs a written agreement.
How much does it cost per email verified?
There is no per-check price. Capacity is sold in blocks of 500 checks a day at $9.95/month, which is $0.000663 per check, about $0.0007, if every check is used. Add a base plan from $69/month, which includes 500 a day. At full use the all-in cost is $0.00102 per check on Starter with 10 blocks and $0.00068 with 200 blocks.
Can I give my customers their own EmailPal API keys?
No. API keys must not be shared between organisations, and each key acts with the full authority of your account. Your customers use your API and your keys, and you call EmailPal with yours.
What happens when my daily capacity runs out?
EmailPal answers 429 verification_limit_reached, with limit and used in the body. It resets at midnight UTC. There is no Retry-After header on that error, so compute the wait yourself, queue the work, or buy another block of 500 a day. A list that needs more credits than remain is refused whole.
What does a 503 capacity_exhausted mean?
Every verification IP was at its hourly limit, so the probe could not be sent. The check is not counted against your daily allowance. Wait for the number of seconds in the Retry-After header and retry. Queue these rather than failing your own customer.
How many checks a day can I buy?
Your plan allowance (500 on Starter, 1,500 on Growth, 4,500 on Scale) plus up to 200 blocks of 500, which is 100,000 more a day. A purchase can also be refused with 409 capacity_unavailable if EmailPal has no unsold verification capacity at that moment.
Can I use the EmailPal name in my own marketing?
Using the EmailPal name and brand as your own needs a written agreement under Terms section 9. The Terms do not spell out whether naming EmailPal as your supplier is allowed, so ask in writing before you do it. You are also responsible for what your end users do, and your own privacy terms need to cover passing their addresses to a processor.
Related
- List verification
- API reference: POST /verify
- API reference: POST /lists/verify
- Terms of Service (section 9)
- Email verification API: verdicts, limits, pricing, code
- Add email verification to a signup form
- Catch-all emails: how to detect them and whether to send
- EmailPal vs Winnr
- EmailPal vs Instantly
- All use cases
Build it on infrastructure that holds up
Domains, mailboxes, warming and verification over one REST API and MCP server, with the sequencer, Unibox and CRM on every plan.
No card to start — cancel any time.