Webhooks

Your app finds out the moment it happens

Register an endpoint and Pingovo posts to it the instant a verification job finishes, a job fails, your credit balance runs low, or a payment lands. Signed, retried, and delivered without anything on your side having to ask.

Pingovo→Webhook→Your App
POST /webhooks/pingovo200 OK
{
"id": "evt_1757404462_8f21c4k",
"type": "job.completed",
"created": 1757404462,
"data": { … }
}

5

event types available

HMAC

SHA-256 signed payloads

5

retry attempts, maximum

10s

delivery timeout per attempt

Event types available

Subscribe an endpoint to any combination of the five. A sixth, test.event, is fired on demand from the dashboard so you can prove the integration works before it matters.

job.completed

A bulk verification job finished. Carries the job id, the file name, the total checked, and the safe / risky / invalid / unknown split.

job.failed

A verification job stopped before finishing, with the error and when it started.

credits.low

Your balance crossed the low-credit threshold. Carries the remaining count, so a top-up can be automated.

payment.success

A payment settled — transaction id, amount, credits added, and the plan it applied to.

payment.failed

A payment did not go through, so billing state in your own system can follow along.

What webhooks cover — and what they do not

What is a webhook? It is the inverse of an API call. With an API, your code asks our server for news and has to keep asking. With a webhook, our server tells yours the moment something happens — one HTTPS POST carrying the event. For an integration that matters, because news is only useful while it is fresh: polling every fifteen minutes means acting on it fifteen minutes late, every time.

Webhooks carry account and job events: a verification run finishing or failing, a credit balance falling, a payment settling. They are the events your own systems need to react to programmatically, and each one arrives once, signed, with the data already attached.

Campaign engagement is not among them. Opens, clicks, bounces and unsubscribes are recorded against each recipient and belong to the campaign report rather than to an endpoint — you read them in the campaign analytics, on the per-recipient event timeline, or in the CSV export that respects whatever filter you had on screen. Saying so plainly is worth more than a longer event list: an integration built against a callback that never fires is worse than one built against an export that does.

The line between this page and the verification API is direction. The API is your code asking ours a question and waiting for the answer. A webhook is our server telling yours something without being asked. Most integrations use both — submit a list through the API, then let job.completed tell you when it is ready instead of polling for it.

Verifying a delivery

Recompute the digest over the timestamp and the raw body, compare in constant time, and reject anything stale. Roughly ten lines in any language.

const [t, v1] = req.headers['x-webhook-signature']
 .split(',').map(p => p.split('=')[1]);

const expected = crypto
 .createHmac('sha256', SECRET)
 .update(`${t}.${rawBody}`)
 .digest('hex');

// Reject replays before trusting the digest.
if (Math.abs(Date.now() / 1000 - t) > 300) throw …
if (!crypto.timingSafeEqual(…)) throw …

Production-ready webhooks

Pushed, not polled

The POST is dispatched as the event happens. Nothing on your side has to ask on a timer, and nothing waits for the next poll to find out.

Signed with HMAC-SHA256

Every request carries X-Webhook-Signature in the form t=<timestamp>,v1=<hmac>. Recompute it with your secret to prove the call came from Pingovo and is not a replay.

Retried on failure

A non-2xx response or a timeout is retried up to five times, at an interval you set. The failure and its reason are recorded against the endpoint either way.

And one you should not have to ask for: SSRF protection

An endpoint URL is checked against private, loopback and link-local ranges when you register it — and again immediately before every single delivery, because a hostname that resolved to a public address last week can be repointed at an internal one today. Redirects are not followed at all, since a 3xx response is the other way to aim a request somewhere it was never allowed to go.

How to integrate Pingovo webhooks

1

Register an endpoint

Add an HTTPS URL and choose which event types it should receive. Pingovo issues a signing secret for that endpoint — store it somewhere your server can read it.

2

Receive the POST

Each delivery is a JSON body with an event id, a type, a created timestamp and a data object, alongside X-Webhook-Signature, X-Webhook-Timestamp and X-Webhook-Event headers.

3

Verify before you trust it

Recompute the HMAC over "<timestamp>.<body>" with your secret and compare. Anything older than five minutes should be rejected as a replay, whatever the signature says.

4

Act on it

Pull the verified list into your CRM, trigger a top-up when credits run low, or update billing state. Return a 2xx quickly — the delivery has a ten-second timeout.

You can see what happened to every delivery

The worst property a webhook integration can have is silence. Something fired, your endpoint was mid-deploy, and nobody finds out until a customer asks why their list never synced — because the only record of the attempt was a log line on a server you do not own.

Each attempt is recorded against the endpoint with the response it got. A 500, a timeout, a connection refused — each is kept with its reason, so a run of failures is visible as a run of failures rather than as an absence of events. Fire a test delivery any time to confirm an endpoint is healthy without waiting for a real one.

  • Retries continue until the attempt limit you configured is reached
  • Any status outside 2xx counts as a failure, as does exceeding the ten-second timeout
  • An endpoint can be deactivated without being deleted while you fix it
https://api.acme.com/hooks/pingovoActive

Recent deliveries

job.completedattempt 1200
credits.lowattempt 1200
job.completedattempt 3200
job.completedattempt 2timeout
job.completedattempt 1500
Read bottom-up: two failures, then the retry that landed.

Frequently asked questions

A webhook is the inverse of an API call. With an API, your code asks our server for news and has to keep asking. With a webhook, our server tells yours the moment something happens — one HTTPS POST carrying the event, its type and its data. You register a URL, choose the events you care about, and Pingovo calls you.

Five: job.completed and job.failed for bulk verification jobs, credits.low when your balance crosses the warning threshold, and payment.success and payment.failed for billing. There is also a test event you can fire from the dashboard at any time to check your endpoint end to end before you rely on it.

No. Open, click, bounce and unsubscribe events are recorded against each recipient and surfaced in the campaign report, the per-recipient event timeline and the CSV export — they are not pushed to an endpoint. If you need that data in another system, export it or read it through the API rather than waiting for a callback that will not arrive.

Every request carries an X-Webhook-Signature header of the form t=<unix timestamp>,v1=<hex digest>. Recompute an HMAC-SHA256 over the string "<timestamp>.<raw JSON body>" using the endpoint secret, and compare with a constant-time comparison. Reject anything whose timestamp is more than five minutes old, which is what stops a captured request being replayed later.

The delivery is retried. Each endpoint has a retry policy — up to five attempts, spaced by an interval you choose — and any response outside 2xx, or a request that exceeds the ten-second timeout, counts as a failure. If every attempt fails, the failure and its reason are recorded against the endpoint so you can see what happened rather than guessing.

You cannot, deliberately. URLs resolving to private, loopback or link-local ranges are rejected — and re-checked immediately before every delivery, not just at registration, because a hostname that passed once can be repointed at an internal address afterwards. Redirects are not followed either, since a 3xx is the other way to smuggle a request somewhere it should not go.

Start for free

Register your webhook endpoint and start receiving signed events today.

We use essential cookies to run this site, and optional functional/analytics cookies to improve it. See our Privacy Policy for details.