Webhooks and API quickstart
SendSure supports two common developer entry points: synchronous verification through the API and asynchronous follow-up through webhooks or downstream processing.
Create an API key
Go to the integrations or API key area in the dashboard and generate a credential dedicated to the workload you are about to build.
Use separate keys for:
- production traffic
- staging or QA
- customer-specific or environment-specific integrations
That separation makes incident response and key rotation much simpler.
Send your first request
Start with one request and verify the shape of the response before you write wrappers around it.
curl -X POST "https://api.sendsure.ai/api/verify/single" \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"email":"alice@example.com"}'
Confirm that your integration handles both high-confidence outcomes and ambiguous ones.
Working in Node.js or Python? Skip the HTTP plumbing entirely — the official SDKs wrap these endpoints with typed clients, automatic rate-limit handling, and the webhook signature helpers described below.
Webhook sanity checks
If you are consuming webhooks:
- verify the signature (use the v2 scheme below)
- return
2xxquickly - push heavy work into a queue
- deduplicate by event id, and log both event IDs and timestamps
Webhook failures are usually caused by slow handlers, incorrect secrets, or endpoints that are not reachable from the public internet.
Webhook delivery headers
Every webhook delivery (including test deliveries) is an HTTP POST with a JSON body and these headers:
| Header | Value | Purpose |
| --- | --- | --- |
| X-SendSure-Event-Id | UUID, same as the payload id field | Deduplicate deliveries — retries reuse the same id |
| X-SendSure-Event | Event type, e.g. job.completed | Route the event without parsing the body |
| X-SendSure-Timestamp | Unix seconds at send time | Enforce a replay tolerance window |
| X-SendSure-Signature-V2 | t={timestamp},v2={hex} | Recommended. Replay-resistant HMAC-SHA256 signature |
| X-SendSure-Signature | sha256={hex} | Legacy HMAC-SHA256 signature of the raw body (kept for backward compatibility) |
| Content-Type | application/json | |
| User-Agent | SendSure-Webhook/1.0 | |
Verify the v2 signature (recommended)
The v2 signature is computed as HMAC-SHA256(secret, "{timestamp}.{rawBody}"), where timestamp is the t= value from the header and rawBody is the exact raw request body bytes. Because the timestamp is inside the signed message, a captured request cannot be replayed later.
Always verify against the raw body — do not re-serialize parsed JSON.
const crypto = require('crypto');
const TOLERANCE_SECONDS = 300; // reject deliveries older than 5 minutes
function verifyV2(rawBody, signatureHeader, secret) {
// signatureHeader looks like: t=1751500800,v2=5f8a...
const parts = Object.fromEntries(
signatureHeader.split(',').map((kv) => kv.split('=', 2))
);
const timestamp = Number(parts.t);
const signature = parts.v2;
if (!Number.isFinite(timestamp) || !signature) return false;
// 1. Replay protection: reject stale (or far-future) timestamps.
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > TOLERANCE_SECONDS) return false;
// 2. Constant-time signature comparison.
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return (
expected.length === signature.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
);
}
// Express example — capture the raw body before JSON parsing:
// app.post('/webhooks/sendsure', express.raw({ type: 'application/json' }), (req, res) => {
// const ok = verifyV2(req.body.toString('utf8'), req.get('X-SendSure-Signature-V2'), SECRET);
// if (!ok) return res.status(401).end();
// res.status(200).end(); // ack fast, process async
// });
Verify the legacy signature
The original X-SendSure-Signature header is still sent on every delivery and is unchanged: sha256={hex} where hex = HMAC-SHA256(secret, rawBody). It has no timestamp, so it is not replay-resistant — prefer v2 for new integrations.
const crypto = require('crypto');
function verifyLegacy(rawBody, signatureHeader, secret) {
const expected =
'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return (
expected.length === signatureHeader.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader))
);
}
Idempotent processing
Deliveries are retried with exponential backoff on non-2xx responses, timeouts, and network failures — so your endpoint will occasionally receive the same event more than once. Retries carry the same X-SendSure-Event-Id (and the same id field inside the payload).
Make processing idempotent:
- read
X-SendSure-Event-Id(or the payloadid) - check it against a store of recently processed ids (a unique-keyed table or a Redis
SET NXwith a TTL of a day works well) - if already seen, return
2xximmediately without reprocessing - otherwise record the id and process the event
Returning 2xx for duplicates is important — a non-2xx response would just trigger more retries of an event you already handled.
Rotating your signing secret
Rotate your secret if it may have leaked, when staff with access leave, or on a regular schedule.
- Dashboard: Integrations → Webhooks → the "Rotate secret" button next to the secret display.
- API:
POST /api/webhooks/rotate-secret(authenticated). The response contains the new secret exactly once — it cannot be retrieved again.
Rotation is immediate: the next delivery is signed with the new secret, and deliveries signed with the old secret will fail verification. Update the secret in your receiver first/immediately after rotating. If your receiver supports it, accept both secrets for a short overlap window while you roll out the change.
Rate-limit and retry posture
Build for graceful retries. Even if the API is healthy, your own network and downstream services will not be perfect.
Recommended posture:
- time out client calls intentionally
- retry idempotent operations
- log request IDs and relevant status codes
- isolate verification errors from the rest of your signup flow
What to include in a support request
If you need API help, bring:
- the endpoint you called
- approximate timestamp
- HTTP status code
- a redacted request sample
- whether the issue happens in staging or production
That is usually enough for support to narrow the failure domain quickly.