API documentation
Two APIs share one base URL: verification, paid with verification credits, and sending, paid with email credits. Both speak JSON over HTTPS.
Overview
The base URL for every request is https://wisesender.com. Requests and responses are JSON, encoded as UTF-8. All endpoints allow cross-origin calls, but keep your keys on your server: a key in browser code can be copied by anyone.
| Endpoint | What it does | Billed in |
|---|---|---|
| GET /v1/verify | Check one address | Verification credits |
| POST /v1/verify/bulk | Check up to 1,000 addresses | Verification credits |
| POST /v1/email/send | Send transactional email | Email credits |
Authentication
Create keys in the dashboard. Each key is shown once; only a hash is stored, so copy it somewhere safe. You can hold up to 10 keys of each type and revoke any of them instantly.
| Key type | Starts with | Send it as |
|---|---|---|
| Verification key | ws_live_ | X-API-Key header, or ?key= in the URL |
| Sending key | ws_send_ | Authorization: Bearer header, or X-API-Key |
Verify one email
| Parameter | Description |
|---|---|
| Required. The address to check. | |
| mode | power (default) checks the mailbox with the receiving server and takes a few seconds. quick skips the mailbox and answers in under a second. |
curl "https://wisesender.com/v1/[email protected]&mode=power" \ -H "X-API-Key: ws_live_YOUR_KEY"
const url = 'https://wisesender.com/v1/verify?email=' + encodeURIComponent(email); const res = await fetch(url, { headers: { 'X-API-Key': process.env.WISESENDER_KEY } }); const result = await res.json();
r = requests.get("https://wisesender.com/v1/verify", params={"email": email, "mode": "quick"}, headers={"X-API-Key": KEY}, timeout=30) result = r.json()
$ch = curl_init('https://wisesender.com/v1/verify?email=' . urlencode($email)); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['X-API-Key: ' . $key]]); $result = json_decode(curl_exec($ch), true);
Verify in bulk
Send up to 1,000 addresses in one request. Duplicates are removed before charging. The request waits until every address is checked, so allow a long timeout in power mode; for bigger lists, upload a file in the dashboard instead.
curl https://wisesender.com/v1/verify/bulk \ -H "X-API-Key: ws_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "emails": ["[email protected]", "[email protected]"], "mode": "power" }'
{
"count": 2,
"summary": { "safe": 1, "risky": 0, "invalid": 1 },
"credits_refunded": 0,
"results": [ { "email": "[email protected]", "status": "safe", ... }, ... ]
}
The result object
| Field | Meaning |
|---|---|
| status | safe, risky or invalid. The short answer. |
| category | The detailed verdict. See categories. |
| score | 0 to 100. Higher is more likely to be delivered. |
| reason | A plain-language explanation, e.g. "Mailbox does not exist". |
| safe_to_send | true for safe and role results. |
| syntax | The address is well formed. |
| domain_exists | The domain resolves. null if DNS failed. |
| mx_found | The domain publishes mail servers. |
| mx_records | Mail server host names, best first. |
| mailbox_exists | The server confirmed the inbox. null if not checked or not answered. |
| mailbox_full | The inbox exists but is over quota. |
| catch_all | The domain accepts every address. |
| disposable | A throwaway inbox provider. |
| role_account | A shared address such as info@ or sales@. |
| free_provider | Gmail, Outlook, Yahoo and similar. |
| suggestion | The likely intended domain for a typo, or null. |
| smtp_note | The mail server's own reply, for debugging. |
| mode | The mode that was used. |
Categories
| category | status | score | What to do |
|---|---|---|---|
| safe | safe | 90 to 95 | Send. |
| role | safe | 75 | Send; expect lower engagement. |
| valid_domain | risky | 60 | Quick mode only. Run power mode before a campaign. |
| catch_all | risky | 55 | Send carefully, watch bounces. |
| unknown | risky | 40 | Retry later. Refunded in power mode. |
| inbox_full | risky | 25 | Retry later. |
| disposable | risky | 10 | Don't send. |
| invalid | invalid | 0 | Remove from your list. |
Send an email
The from domain must be verified under Transactional email, Domains. Without cc or bcc, each to address receives its own copy and can carry its own merge_data. With cc or bcc, one message goes to everyone.
| Field | Description |
|---|---|
| from | Required. "[email protected]" or "Acme <[email protected]>". |
| to | Required. An address, or a list. Items may be objects: { "email", "name", "merge_data" }. |
| cc, bcc | Optional. Same formats as to. 50 recipients in total. |
| subject | Required unless a template is used. Up to 500 characters. |
| html, text | At least one, unless a template is used. 500 KB in total. |
| template_key | Send a saved template, e.g. tpl_3f9a1c2b7d4e. |
| merge_data | Values for {{placeholders}}, shared by all recipients. |
| reply_to | Optional reply address. |
| attachments | See attachments. |
| tags | Up to 10 key/value pairs, returned in logs and webhooks. |
| verify | true runs a quick check first and skips invalid addresses (not charged). |
| unsubscribe_url | An https URL added as a one-click List-Unsubscribe header. |
curl https://wisesender.com/v1/email/send \ -H "Authorization: Bearer ws_send_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "Acme <[email protected]>", "to": [{ "email": "[email protected]", "name": "Jane", "merge_data": { "code": "482913" } }], "subject": "Your login code", "html": "<p>Hi {{name}}, your code is <b>{{code}}</b></p>", "tags": { "type": "otp" } }'
{
"data": [
{ "id": "1842", "to": "[email protected]", "type": "to", "status": "sent" }
],
"sandbox": false
}
Each recipient's status is sent, suppressed (on your do-not-send list), invalid (failed the optional check) or failed. Only sent uses an email credit.
Templates
Create templates in the dashboard and send them with template_key. Placeholders use double braces and support dotted paths:
| {{name}} | Inserted with HTML escaping. Safe for user-supplied values. |
| {{order.id}} | Reads nested values from merge_data. |
| {{{html_block}}} | Triple braces insert the value as raw HTML. Use only with trusted content. |
Unknown placeholders become empty text. A recipient's own merge_data overrides the shared values, and name and email are filled from the recipient automatically.
Attachments
Pass a list of objects with a file name and base64 content. Up to 10 files and 10 MB in total. Executable file types are refused because mailbox providers block them.
"attachments": [
{ "name": "invoice-10482.pdf", "content": "JVBERi0xLjQK..." },
{ "name": "logo.png", "content": "iVBORw0KGgo...", "content_id": "logo" }
]
// reference the inline image in html as <img src="cid:logo">SMTP relay
Use the relay when your software only speaks SMTP. The same domain checks, limits, logs and webhooks apply, and Bcc headers are removed before delivery.
| Host | smtp.wisesender.com |
| Port | 587 with STARTTLS, or 465 with TLS |
| Username | wisesender |
| Password | A sending key (ws_send_...) |
Webhooks
Set an https URL under Transactional email, Settings. We POST one JSON event per recipient for sent, delivered, bounced, soft_bounced, complained, delayed, failed, opened and clicked.
{
"event": "delivered",
"message_id": "1842",
"agent": "OTP",
"from": "[email protected]",
"to": "[email protected]",
"recipient_type": "to",
"subject": "Your login code",
"tags": { "type": "otp" },
"timestamp": "2026-11-10T09:14:03.120Z"
}Check the signature
Every request carries X-WiseSender-Signature: sha256=<hex>, an HMAC-SHA256 of the raw body using your webhook secret. Compare it before trusting the event.
const crypto = require('node:crypto'); function isValid(rawBody, header, secret) { const good = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); return header?.length === good.length && crypto.timingSafeEqual(Buffer.from(header), Buffer.from(good)); }
$raw = file_get_contents('php://input'); $good = 'sha256=' . hash_hmac('sha256', $raw, $secret); if (!hash_equals($good, $_SERVER['HTTP_X_WISESENDER_SIGNATURE'] ?? '')) { http_response_code(401); exit; }
Errors
Errors return a non-2xx status and a body like { "error": "Invalid API key" }.
| Status | Meaning |
|---|---|
| 400 | A field is missing or invalid. The message names it. |
| 401 | The key is missing, wrong or revoked. |
| 402 | Not enough credits. Buy a pack or wait for the daily refill. |
| 403 | The sending domain is not verified, or sending is paused for the account. |
| 404 | The endpoint or template does not exist. |
| 413 | Too many addresses, recipients or bytes in one request. |
| 429 | A rate or daily limit was reached. Slow down or try tomorrow. |
Limits
| Bulk verify | 1,000 addresses per request; 20,000 per file in the dashboard |
| Free checks without a key | 100 per IP address per day |
| Send API | 300 requests a minute per account, 50 recipients per request |
| Daily sending | 200 emails a day for new accounts, raised as your bounce and complaint rates stay low |
| Automatic pause | Above 5% bounces or 0.3% complaints over 7 days, after 100 sends |
Need a higher limit? Ask support with your expected volume.