/api/v1/email-validation/jobsvalidateCreate an email-validation job from an address list or CSV.
The agent storefront at https://agents.tuski.ai/llms.txt?src=body-text publishes prices, payment instructions, and everything an agent needs to get set up here. Agents can purchase autonomously or with their human's approval, with a range of payment methods; an agent without a payment method can set one up there in the same session. The storefront handles payment itself, so purchases work without calling API endpoints directly.
Build email validation into your application with ordinary HTTPS endpoints, or connect an AI assistant through MCP. Both use the same Tuski API keys.
Send your tsk_live_... key as an Authorization: Bearer <key> header. Create and revoke keys in the authenticated API-key dashboard. The full secret is shown only once, so store it securely and never embed it in browser code.
Grant validate to create or cancel jobs and export to retrieve results.
API keys can be given an expiry of 1 to 3650 days (the dashboard offers 30 days, 90 days or 1 year); an expired key answers 401 with code api_key_expired.
Base URL: https://tuski.ai. Jobs and searches run asynchronously: start one, poll its status, then retrieve JSON or CSV results. All routes are tenant-scoped. Full schema at /api/v1/openapi.json, or read the generated API reference for every endpoint, parameter and response rendered from that same document.
/api/v1/email-validation/jobsvalidateCreate an email-validation job from an address list or CSV.
/api/v1/email-validation/jobsauthenticatedList the account's jobs, newest first.
/api/v1/email-validation/jobs/{job_id}authenticatedStatus, counters and ETA of a job.
/api/v1/email-validation/jobs/{job_id}/resultsexportResult rows as JSON or a bounded CSV export.
/api/v1/email-validation/jobs/{job_id}/pausevalidatePause a running validation job.
/api/v1/email-validation/jobs/{job_id}/resumevalidateResume a paused validation job.
/api/v1/email-validation/jobs/{job_id}/cancelvalidateCancel a validation job.
/api/v1/creditsauthenticatedBalance of the organization the key is bound to.
/api/v1/discovery/searches/estimatediscoveryPrice of an AI Web Search before spending anything.
/api/v1/discovery/searchesdiscoveryStart a company search, reserves credits up front.
/api/v1/discovery/searches/{run_id}discoveryStatus of a company search.
/api/v1/discovery/searches/{run_id}/resultsdiscoveryCompany search result rows.
/api/v1/discovery/searches/{run_id}/canceldiscoveryCancel a company search, releases the unused reservation.
/api/v1/discovery/searches/{run_id}/contactscontactsStart a free people search for a company search's matched companies.
/api/v1/discovery/contact-jobs/{job_id}/contactscontactsContacts job result rows, 25 per page by default, 100 at most.
/api/v1/discovery/contact-jobs/{job_id}/unlock-estimatecontactsPrice of unlocking contact fields before spending anything.
/api/v1/discovery/contact-jobs/{job_id}/unlockcontactsUnlock email, LinkedIn or phone for specific people, charged per field.
/api/v1/discovery/contact-unlockscontactsStatus of a set of contact unlock requests.
curl -i -X POST https://tuski.ai/api/v1/email-validation/jobs \
-H "Authorization: Bearer $TUSKI_API_KEY" \
-H "Idempotency-Key: import-2026-01" \
-H "Content-Type: application/json" \
--data '{"emails":["person@example.com"]}'curl -sS \ -H "Authorization: Bearer $TUSKI_API_KEY" \ "https://tuski.ai/api/v1/email-validation/jobs/123/results?format=json&limit=1000&offset=0"
emails or csv_text.Idempotency-Key is required when creating a job, and is scoped to your account plus organization, not the individual API key — a retry from a teammate's key in the same organization returns the same job instead of a second charge.201; an exact replay returns 200 without a second debit or dispatch.409.limit 1-5000, offset, and optional status; has_more reflects whether a next page actually has rows.format=csv for a real text/csv download; since the CSV body has no has_more field, truncation is signaled with an X-Total-Count header and, when more rows remain, a Link: rel="next" header.offset for more.get_results_csv tool (and get_results with format=csv) embeds up to 500 rows inline; a larger export comes back as a download link valid for 10 minutes instead.Every error body is { "detail": "<message>", "code": "<machine_code>" }. Every error body carries a machine-readable `code` field next to `detail`. Requests sent with the default Python-urllib User-Agent are rejected by the CDN, so set an explicit User-Agent.
400 / 422Malformed request or invalid input.
401Missing, invalid, inactive, or revoked API key.
402The organization does not have enough credits.
403The API key lacks the required permission.
404The job is unavailable; cross-tenant jobs are never disclosed.
409The Idempotency-Key was reused with a different payload.
413The request or export exceeds a documented size limit.
429One common API-key bucket is shared across endpoints and IPs; follow Retry-After.
500Unexpected server failure; retry safely and contact support if persistent.
Configure one HTTPS URL per organization in organization settings (admins and owners only) and Tuski calls it when a job or search finishes. Every request is signed so you can verify it came from Tuski.
validation_job.completedAn email-validation job finished (status "done") or was cancelled (status "cancelled").
company_search.completedA company search finished ("succeeded"), failed, or was cancelled.
validation_job.verdict_changedA result of a finished validation job changed later (a late bounce, or a probe Gmail turned out not to have sent): carries the job id, lead id, old and new status.
webhook.testA small ping (event, organization_id, sent_at) queued by "Send test event" in organization settings so you can check your endpoint and signature code.
Tuski-EventThe event type, e.g. validation_job.completed.
Tuski-DeliveryA delivery id, stable across retries of the same event. Use it to de-duplicate.
Tuski-Signaturet=<unix seconds>,v1=<hex> - v1 is HMAC-SHA256(secret, "<t>." + raw request body).
{
"event": "validation_job.completed",
"job_id": 1234,
"status": "done",
"total": 500,
"queued": 0,
"sent": 500,
"valid": 421,
"invalid": 38,
"unknown": 12,
"unreachable": 9,
"mailbox_full": 4,
"blocked": 6,
"soft_temp": 5,
"routing_error": 3,
"other_bounce": 2,
"created_at": "2026-09-21T10:02:00Z",
"finished_at": "2026-09-21T10:07:41Z",
"links": {
"status": "https://tuski.ai/api/v1/email-validation/jobs/1234",
"results": "https://tuski.ai/api/v1/email-validation/jobs/1234/results"
}
}{
"event": "company_search.completed",
"search_id": "42",
"status": "succeeded",
"target": 50,
"result_count": 48,
"billable_results": 45,
"reserved_credits": 50,
"settled_credits": 45,
"created_at": "2026-09-21T09:40:00Z",
"completed_at": "2026-09-21T09:52:18Z",
"links": {
"status": "https://tuski.ai/api/v1/discovery/searches/42",
"results": "https://tuski.ai/api/v1/discovery/searches/42/results"
}
}Verify every request before trusting it: read the raw request body, parse t and v1 from Tuski-Signature, reject a timestamp more than 5 minutes old, recompute the HMAC-SHA256 over "<t>." + body, and compare the result to v1 in constant time.
import hashlib
import hmac
import time
def verify_tuski_signature(secret: str, raw_body: bytes, signature_header: str) -> bool:
# raw_body must be the exact request bytes, read before any JSON parsing.
parts = dict(part.split("=", 1) for part in signature_header.split(","))
timestamp, signature = parts["t"], parts["v1"]
if abs(time.time() - int(timestamp)) > 300:
return False # older than 5 minutes - reject as a possible replay
signed_payload = f"{timestamp}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature) # constant-time comparison
const crypto = require('crypto');
function verifyTuskiSignature(secret, rawBody, signatureHeader) {
// rawBody must be a Buffer of the exact request bytes, read before JSON.parse.
const parts = Object.fromEntries(signatureHeader.split(',').map((part) => part.split('=')));
const { t: timestamp, v1: signature } = parts;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return false; // older than 5 minutes - reject as a possible replay
}
const signedPayload = Buffer.concat([Buffer.from(`${timestamp}.`), rawBody]);
const expected = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex');
const expectedBuf = Buffer.from(expected, 'utf8');
const signatureBuf = Buffer.from(signature, 'utf8');
// timingSafeEqual throws on a length mismatch, so check that first.
if (expectedBuf.length !== signatureBuf.length) return false;
return crypto.timingSafeEqual(expectedBuf, signatureBuf); // constant-time comparison
}
Tuski exposes a Streamable HTTP MCP server at https://tuski.ai/mcp with tools covering email validation and company search. Claude Desktop and claude.ai connect with an OAuth connector; other clients send the same API key as a Bearer header. A list larger than about 2000 addresses is better uploaded as a file through REST.
Troubleshooting
get_creditsauthenticatedBalance of the organization the credential is bound to: the number returned is that organization wallet, not a personal balance.
validate_emailsvalidateStart a validation job, 1 credit per accepted address.
wait_for_jobauthenticatedWait up to 50 seconds for a job and report progress.
get_job_statusauthenticatedStatus, counters and ETA of a job.
get_resultsexportResult rows as JSON, 100 per page by default, 1000 at most.
get_results_csvexportResults as CSV: up to 500 rows inline, larger exports as a download link valid for 10 minutes.
list_jobsauthenticatedThe account's jobs, newest first.
pause_jobvalidatePause a running validation job.
resume_jobvalidateResume a paused validation job.
cancel_jobvalidateCancel a validation job.
estimate_company_searchdiscoveryPrice of an AI Web Search before spending anything.
start_company_searchdiscoveryStart a company search, reserves credits up front.
get_company_searchdiscoveryStatus of a company search.
wait_for_company_searchdiscoveryWait for a company search to finish instead of polling get_company_search.
get_companiesdiscoveryCompany search result rows, 25 per page by default, 100 at most.
export_companiesdiscoveryCompany search results as a file.
cancel_company_searchdiscoveryCancel a company search, releases the unused reservation.
estimate_contact_unlockcontactsPrice of unlocking contact fields before spending anything.
find_contactscontactsStart a free people search for the matched companies of a company search.
get_contactscontactsContacts job result rows, 25 per page by default, 100 at most.
unlock_contactscontactsUnlock email, LinkedIn or phone for specific people, charged per field.
wait_for_contactscontactsWait for a set of contact unlocks to finish instead of polling.
{
"mcpServers": {
"tuski": {
"url": "https://tuski.ai/mcp",
"headers": {
"Authorization": "Bearer <API_KEY>"
}
}
}
}Call get_credits first to see the available balance before spending it. To validate emails: call validate_emails to start a job, then call wait_for_job repeatedly, following the poll_after_seconds it returns instead of polling quickly, until the job finishes. Then call get_results and report the real counts and rows it returns. To search for companies: call estimate_company_search first to see the price before spending anything, then call start_company_search. Call wait_for_company_search the same way as wait_for_job, then call get_companies or export_companies and report the real results. If validate_emails or start_company_search fails or times out, repeat the call with exactly the same arguments: within 10 minutes that returns the existing job and nothing is charged again. Do not change the input while retrying, because different input starts a new job and a new charge.