skink.dev
API reference

First verify in under 5 minutes.

curl -s https://api.skink.dev/v1/verify \
  -H "Authorization: Bearer $SKINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"jane@acme.com"}'

{ "status": "deliverable", "confidence": 0.91, "safe_to_send": true, "disposable": false, "recommendation": "send" }
01 — setup

Get a key.

  • Sign up at app.skink.dev.
  • 100 free credits. No card. They never expire.
  • Create an API key in the dashboard. It is shown once.

Server-side only

A leaked key spends your credits. If exposed, revoke it from the dashboard.

02 — request

Verify.

POST/v1/verify

One POST, two headers, a JSON body. Pick your language.

curl -s https://api.skink.dev/v1/verify \
  -H "Authorization: Bearer $SKINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"jane.doe@acme.com"}'
03 — response

Read the result.

{
  "email": "jane.doe@acme.com",
  "status": "deliverable",
  "sub_status": "catchall_resolved",
  "confidence": 0.91,
  "did_you_mean": null,
  "free": false,
  "role": false,
  "disposable": false,
  "safe_to_send": true,
  "accept_all": true,
  "risk_score": 0.04,
  "mx_provider": "microsoft365",
  "signals": {
    "syntax_valid": true,
    "mx_found": true,
    "is_catchall": true,
    "catchall_resolution": "verified",
    "reasons": [
      "SMTP accepted (250)",
      "Catch-all domain — SMTP alone cannot determine individual mailbox existence",
      "Additional verification checks resolved mailbox existence"
    ]
  },
  "recommendation": "send",
  "model_version": "v0-stage1-4",
  "processed_at": "2026-09-25T14:23:01Z",
  "duration_ms": 412
}

Which field do I read?

safe_to_send
One boolean, one decision. Branch on this to send or not.
recommendation
send, risky_send or suppress. Use it to route risky_send to a slower lane.
status
What we found. Use it for list cleaning and reporting.
disposable
Temp-mail or burner address. Independent of the rest. Gate signups on it.

safe_to_send: start here

  • true for send, and for risky_send at confidence 0.5 or higher.
  • false for suppress, disposable and deferred results.
  • Not a delivery guarantee. A full mailbox still bounces. For list cleaning, filter on status.

Verdict

status
deliverable, undeliverable, risky or unknown.
sub_status
Stable reason key for the status. Treat it as an open set.
confidence
0 to 1. How certain we are the mailbox exists.
recommendation
send, risky_send or suppress.
safe_to_send
Boolean you can branch on directly.

Address flags

disposable
Known temp-mail or burner domain.
role
Role address such as info@ or sales@.
free
Free-mail provider such as gmail.com.
accept_all
The domain accepts mail for any local part.
did_you_mean
Suggested correction for a likely typo, else null.

disposable covers a 74k-domain list refreshed continuously, plus signals that catch a new burner domain before it is listed.

Risk and detail

risk_score
0 to 1 fraud and abuse risk. Separate from confidence.
risks
Risk flags. Absent when none apply.
mx_provider
Detected mailbox provider or gateway, else generic or none.
signals.reasons
Plain-English explanation of the verdict.
job_id
Only with allow_async_followup on a deferred result. Poll GET /v1/verify/{job_id}.
04 — reference

Status reference.

status

deliverable
Safe to send.
undeliverable
Hard bounce. Do not send.
risky
Role address, full mailbox, or an unresolved catch-all.
unknown
No verdict: timeout, greylist or DNS failure. Free.

sub_status values are stable keys for parsing. Treat the set as open and ignore values you do not recognize.

Catch-all

catchall_resolved
Domain accepts all mail. Verdict reached from additional checks.
catchall_uncertain
Domain accepts all mail. Could not be confirmed.

Mailbox

invalid_mailbox
Mailbox does not exist.
mailbox_full
Mailbox exists but cannot receive.
disposable
Temp-mail or burner domain.
risk_signal_downgrade
Downgraded from deliverable for a risk reason.

Domain

syntax_error
Malformed address.
no_mx
Domain has no mail server.
dns_failure
DNS lookup failed.

No verdict

probe_inconclusive
No clear answer from the receiving server.
greylist_timeout
Server deferred. Retry later.
smtp_throttled
Receiving server rate-limited the check. Retry later.
smtp_skipped
Live check skipped by fast_fail.
timeout
No verdict within timeout_ms.
05 — request

Request options.

All optional, in the JSON body of POST /v1/verify. fast_fail and skip_catchall_probes also work on /v1/bulk.

timeout_msinteger, 100 to 30000
Maximum time spent on one address. Lower is faster but can return a coarser verdict.default 15000
fast_failboolean
Skips the live mailbox check. Well under 100 ms. Anything not caught by syntax, DNS or disposable checks returns unknown with sub_status smtp_skipped.default false
skip_catchall_probesboolean
Returns a catch-all domain as risky right away instead of resolving it.default false
effortstandard or thorough
thorough spends longer on hard cases. More latency, same credit cost. Not allowed with fast_fail. Raise timeout_ms to match.default standard
min_confidencenumber, 0.35 to 0.9
Sets the safe_to_send bar for risky_send results. Never overrides suppress, disposable or deferred results.default 0.5
allow_async_followupboolean
On a deferred result, returns a job_id. Poll GET /v1/verify/{job_id} for the upgraded verdict.default false
retainboolean
false stores nothing about this verification.default true

Only need to know if it is disposable?

Send fast_fail and read disposable

  • Answers in milliseconds from the disposable-domain list. The mailbox is never checked.
  • Unlisted addresses return unknown, which is free.
  • A disposable hit is billed like any known result.
  • Without fast_fail, a full verification also catches burner domains not yet listed.
  • Works the same on /v1/bulk. Every result row carries disposable.
curl -s https://api.skink.dev/v1/verify \
  -H "Authorization: Bearer $SKINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"user@mailinator.com","fast_fail":true}'

{ "status": "risky", "sub_status": "disposable", "disposable": true,
  "recommendation": "suppress", "safe_to_send": false }
06 — lists

Bulk & feedback.

  1. 1

    Submit

    POST/v1/bulk

    Up to 50,000 emails in source.emails, or an uploaded source.file_id. Returns 202 with a job.

  2. 2

    Track

    GET/v1/bulk/{jobId}

    Poll for status, or pass a callback_url when you submit.

  3. 3

    Fetch

    GET/v1/bulk/{jobId}/results

    Same fields as a single verify, without signals. Available for 24 hours after completion.

Bounce feedback

POST/v1/webhooks/bounce

Report what happened to a send. Body: email, outcome, and optional verified_at in ISO 8601.

hard_bounce
Permanent failure.
soft_bounce
Temporary failure.
complaint
Recipient marked the message as spam.
delivered
Accepted for delivery.
curl -s https://api.skink.dev/v1/bulk \
  -H "Authorization: Bearer $SKINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"source":{"emails":["a@acme.com","b@acme.com"]},"callback_url":"https://example.com/hook"}'
07 — operations

Billing, limits, errors.

Cost
1 credit per verification.
Unknown results
Free.
Repeat checks
Same address within 24 hours may return faster and is billed again.
Rate limit
Per API key. Over the limit: 429 with a Retry-After header.

Errors

Errors are JSON with error and message.

400 bad_request
Invalid body or conflicting options.
401 unauthorized
Missing or invalid API key.
402 payment_required
Out of credits.
403 scope_insufficient
Key is not allowed to use this endpoint.
404 not_found
Unknown job or file.
429 rate_limited
Per-key request limit reached.
08 — clients

SDKs.

Plain HTTPS and JSON works everywhere and needs nothing installed. Official SDKs for Node.js, Python and Ruby wrap the same API.

npm install @skink/sdk

Have a question this page didn’t answer?

See the FAQ