Everything you need to build on WiseSender
One base URL, JSON over HTTPS. Verification and contacts use a verification key and verification credits, sending uses a sending key and email credits.
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 |
| GET /v1/lists | Your contact lists | Free |
| POST /v1/lists/{id}/contacts | Add someone to a list | Free, 1 verification credit with verify |
| 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 (verify and contacts) | ws_live_ | X-API-Key header, Authorization: Bearer, or ?key= for verify |
| 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. |
Lists and contacts
Add people to your contact lists from your own app, a CRM or an automation tool. Contacts added this way behave like a signup on your hosted form: lists with double opt-in email a confirmation first, and the list's welcome email goes out once they're subscribed.
Returns your lists with their IDs: { "lists": [{ "id": 12, "name": "Newsletter", "subscribed": 2140, "double_optin": true }] }
| Field | Description |
|---|---|
| Required. The address to add. | |
| name | Optional, up to 100 characters. |
| verify | Optional. true runs a quick check first and refuses typos, dead domains and throwaway inboxes with a 422. Uses 1 verification credit. |
curl -X POST https://wisesender.com/v1/lists/12/contacts \ -H "X-API-Key: ws_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "[email protected]", "name": "Ann Lee", "verify": true }' # 201 Created (200 if they were already on the list) { "email": "[email protected]", "status": "subscribed", "created": true }
status is subscribed, or pending when a confirmation email was sent.
Pages through a list. status can be subscribed, pending or unsubscribed; limit goes up to 1,000.
Unsubscribes the address and adds it to your do-not-send list, so no campaign or API send reaches it again.
Account events
Besides the delivery events below, your webhook URL receives events about your audience. Automation tools like Zapier, Make and n8n can start a workflow from them.
| Event | When |
|---|---|
| contact.subscribed | Someone joined a list through a form, the API or by confirming |
| contact.unsubscribed | Someone used an unsubscribe link, or you removed them by API |
| list.verified | An uploaded list finished verifying |
| campaign.finished | A campaign was sent, or stopped |
{
"event": "contact.subscribed",
"timestamp": "2026-11-11T09:14:03.120Z",
"data": { "list_id": 12, "list": "Newsletter", "email": "[email protected]", "name": "Ann Lee", "source": "form" }
}They're signed the same way as delivery events. See Webhooks.
Send an email
The from domain must be verified under Domains in the dashboard. 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 in the dashboard under Transactional API, Webhooks. 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, the account email isn't confirmed, or sending is paused. |
| 404 | The endpoint or template does not exist. |
| 413 | Too many addresses, recipients or bytes in one request. |
| 422 | A contact was refused by the verify check. The body says why. |
| 429 | A rate or daily limit was reached. Slow down or try tomorrow. |
Limits
| Bulk verify | 1,000 addresses per request; 100,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 |
| Contacts API | 600 requests a minute per account |
| Daily sending | 200 emails a day for new accounts. Grows as you buy email credits: 2,000 after 10,000, 10,000 after 50,000, 20,000 after 100,000 and 50,000 after 500,000 |
| 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.