Tuski sells to AI agents - live prices and self-serve payment

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.

REST API + Model Context Protocol

Tuski API documentation.

Build email validation into your application with ordinary HTTPS endpoints, or connect an AI assistant through MCP. Both use the same Tuski API keys.

Authentication

One key, two integrations

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.

REST API · v1

Email validation and company search

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.

POST/api/v1/email-validation/jobsvalidate

Create an email-validation job from an address list or CSV.

GET/api/v1/email-validation/jobsauthenticated

List the account's jobs, newest first.

GET/api/v1/email-validation/jobs/{job_id}authenticated

Status, counters and ETA of a job.

GET/api/v1/email-validation/jobs/{job_id}/resultsexport

Result rows as JSON or a bounded CSV export.

POST/api/v1/email-validation/jobs/{job_id}/pausevalidate

Pause a running validation job.

POST/api/v1/email-validation/jobs/{job_id}/resumevalidate

Resume a paused validation job.

POST/api/v1/email-validation/jobs/{job_id}/cancelvalidate

Cancel a validation job.

GET/api/v1/creditsauthenticated

Balance of the organization the key is bound to.

GET/api/v1/discovery/searches/estimatediscovery

Price of an AI Web Search before spending anything.

POST/api/v1/discovery/searchesdiscovery

Start a company search, reserves credits up front.

GET/api/v1/discovery/searches/{run_id}discovery

Status of a company search.

GET/api/v1/discovery/searches/{run_id}/resultsdiscovery

Company search result rows.

POST/api/v1/discovery/searches/{run_id}/canceldiscovery

Cancel a company search, releases the unused reservation.

POST/api/v1/discovery/searches/{run_id}/contactscontacts

Start a free people search for a company search's matched companies.

GET/api/v1/discovery/contact-jobs/{job_id}/contactscontacts

Contacts job result rows, 25 per page by default, 100 at most.

POST/api/v1/discovery/contact-jobs/{job_id}/unlock-estimatecontacts

Price of unlocking contact fields before spending anything.

POST/api/v1/discovery/contact-jobs/{job_id}/unlockcontacts

Unlock email, LinkedIn or phone for specific people, charged per field.

GET/api/v1/discovery/contact-unlockscontacts

Status of a set of contact unlock requests.

CREATE A JOB · CURL
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"]}'
GET RESULTS · JSON
curl -sS \
  -H "Authorization: Bearer $TUSKI_API_KEY" \
  "https://tuski.ai/api/v1/email-validation/jobs/123/results?format=json&limit=1000&offset=0"

Requests and idempotency

  • Create accepts exactly one of emails or csv_text.
  • Tuski writes the probe text itself: each probe is a short, neutral hello picked at random from a rotating set of messages. A request cannot set a subject or body; one sent anyway is ignored.
  • 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.
  • A new job returns 201; an exact replay returns 200 without a second debit or dispatch.
  • Changed input with the same key returns 409.
  • Requests are limited to 25 MB and 100,000 email rows.
  • A small list usually finishes the same day: probes are paced to protect deliverability, each sending mailbox warms up gradually, and results are confirmed only after a 15-minute bounce window and a bounce check of the sending mailbox. A large list takes several days, as each mailbox sends a limited number a day. The confirm page shows the expected finish time before you pay.

Results and limits

  • JSON supports limit 1-5000, offset, and optional status; has_more reflects whether a next page actually has rows.
  • Use 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.
  • REST CSV exports are capped at 5,000 rows per request, paginate with offset for more.
  • The MCP 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.
  • REST and MCP share one hourly rate limit per API key. Keys use their configured hourly limit, or 600 requests/hour by default.
  • Every successful response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset.
  • Add ?wait=<seconds> (up to 50) to a job or search status request to wait for completion instead of polling.
  • The discovery and contacts permissions are off by default: grant them to a key or choose them on the Connect screen.

Errors and retries

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 / 422

Malformed request or invalid input.

401

Missing, invalid, inactive, or revoked API key.

402

The organization does not have enough credits.

403

The API key lacks the required permission.

404

The job is unavailable; cross-tenant jobs are never disclosed.

409

The Idempotency-Key was reused with a different payload.

413

The request or export exceeds a documented size limit.

429

One common API-key bucket is shared across endpoints and IPs; follow Retry-After.

500

Unexpected server failure; retry safely and contact support if persistent.

Webhooks

Get notified instead of polling

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.completed

An email-validation job finished (status "done") or was cancelled (status "cancelled").

company_search.completed

A company search finished ("succeeded"), failed, or was cancelled.

validation_job.verdict_changed

A 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.test

A small ping (event, organization_id, sent_at) queued by "Send test event" in organization settings so you can check your endpoint and signature code.

Request headers

Tuski-Event

The event type, e.g. validation_job.completed.

Tuski-Delivery

A delivery id, stable across retries of the same event. Use it to de-duplicate.

Tuski-Signature

t=<unix seconds>,v1=<hex> - v1 is HMAC-SHA256(secret, "<t>." + raw request body).

Delivery, retries and disabling

  • Deliveries are HTTPS only, on port 443, to a URL that resolves to a public address; redirects are not followed. A 2xx response counts as delivered; answer quickly, because the connection must open within 3 seconds and each wait for response data is cut off after 5 seconds. Anything else is retried.
  • A failed delivery is retried after 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, then given up. Every attempt is signed with the organization's current secret, so after a rotation the failed deliveries still waiting are retried with the new secret.
  • After too many consecutive failures in a row, the webhook is switched off automatically until someone enables it again in organization settings.
  • Payloads carry only ids, statuses, counters and links back to the REST API - never email addresses or other personal data.
VALIDATION_JOB.COMPLETED · EXAMPLE PAYLOAD
{
  "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"
  }
}
COMPANY_SEARCH.COMPLETED · EXAMPLE PAYLOAD
{
  "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.

VERIFY SIGNATURE · PYTHON
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
VERIFY SIGNATURE · NODE.JS
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
}
Model Context Protocol

Connect an AI assistant

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.

Connect Claude Desktop or claude.ai

  1. Open Settings, then Connectors, then "Add custom connector".
  2. Name it "Tuski" and paste the URL exactly: https://tuski.ai/mcp (no trailing period or slash).
  3. Wait for "Checking the server" to finish.
  4. On the next screen, under Authentication choose "Sign in now" (the "No sign-in: Detected" mark is expected for Tuski and is not an error).
  5. Under OAuth client, choose "Use Claude's published identity" (fall back to "Register automatically" if that option is unavailable).
  6. Leave Request headers empty and press Continue.
  7. Sign in to Tuski with Google if asked.
  8. On the Tuski consent page, choose the organization, tick the permissions you want (validate and export are pre-ticked; discovery and contacts are off until ticked), then press Approve.
  9. Back in Claude, ask "what is my Tuski balance" to confirm the connection works.

Troubleshooting

  • The URL check fails with a 404. Check the URL for a trailing period or slash, often left over from pasting out of a sentence.
  • Sign-in never starts. Remove the connector and add it again, and pick "Sign in now" this time.
  • Every tool answers 401. The connector was added with "No sign-in": remove it and re-add it, choosing "Sign in now".
  • A connection was revoked in the dashboard. Claude will ask to sign in again the next time a tool is called; this is expected.
Available operations

Tools for the full job and search lifecycle

get_creditsauthenticated

Balance of the organization the credential is bound to: the number returned is that organization wallet, not a personal balance.

validate_emailsvalidate

Start a validation job, 1 credit per accepted address.

wait_for_jobauthenticated

Wait up to 50 seconds for a job and report progress.

get_job_statusauthenticated

Status, counters and ETA of a job.

get_resultsexport

Result rows as JSON, 100 per page by default, 1000 at most.

get_results_csvexport

Results as CSV: up to 500 rows inline, larger exports as a download link valid for 10 minutes.

list_jobsauthenticated

The account's jobs, newest first.

pause_jobvalidate

Pause a running validation job.

resume_jobvalidate

Resume a paused validation job.

cancel_jobvalidate

Cancel a validation job.

estimate_company_searchdiscovery

Price of an AI Web Search before spending anything.

start_company_searchdiscovery

Start a company search, reserves credits up front.

get_company_searchdiscovery

Status of a company search.

wait_for_company_searchdiscovery

Wait for a company search to finish instead of polling get_company_search.

get_companiesdiscovery

Company search result rows, 25 per page by default, 100 at most.

export_companiesdiscovery

Company search results as a file.

cancel_company_searchdiscovery

Cancel a company search, releases the unused reservation.

estimate_contact_unlockcontacts

Price of unlocking contact fields before spending anything.

find_contactscontacts

Start a free people search for the matched companies of a company search.

get_contactscontacts

Contacts job result rows, 25 per page by default, 100 at most.

unlock_contactscontacts

Unlock email, LinkedIn or phone for specific people, charged per field.

wait_for_contactscontacts

Wait for a set of contact unlocks to finish instead of polling.

MCP CONFIG · STREAMABLE HTTP
{
  "mcpServers": {
    "tuski": {
      "url": "https://tuski.ai/mcp",
      "headers": {
        "Authorization": "Bearer <API_KEY>"
      }
    }
  }
}
AGENT PROMPT
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.
API Documentation — Tuski