API docs

Webhooks

Get told on your own server when the balance runs low, or credits or the own-domain add-on run out.

A webhook is a POST request WeSend sends to your server when something happens. Use it to top up automatically, alert your team, or pause your own sending.

  1. 1

    Add an address

    In the dashboard under Settings, Webhook, add an https:// address and choose the events it receives. A project can have up to 5.

  2. 2

    Keep the secret

    The secret starting with whsec_ is shown once. Keep it in your server's environment.

  3. 3

    Test it

    The Test button sends a webhook.test event and shows your server's answer at once.

Events

EventWhen`data`
balance.lowThe balance fell below the project's threshold. Once per dip.balance, threshold
balance.emptyCredits ran out and sending stopped. Not sent again until the balance is above zero.balance
credits.expiringCredits run out within 7 days. Once per batch of credits.credits, expiresAt, balance
domain.expiringThe own-domain add-on ends in 7 days, and again in 1 day.domain, endsAt, daysLeft
domain.suspendedThe add-on ran out and its grace days passed, so the domain is suspended. Renewed before deletesAt, it comes back at once.domain, deletesAt
balance.low
{
  "id": "evt_9b2f6c1e4a7d4f0e8c3b5a6d7e8f9012",
  "type": "balance.low",
  "createdAt": "2026-10-20T03:15:30.000Z",
  "projectId": "7f3c2a10-5b8e-4d6a-9c1f-2e3d4b5a6c7d",
  "data": {
    "balance": 4200,
    "threshold": 5000
  }
}

id identifies the event. A retried request carries the same id, so store it to avoid handling one event twice.

Headers

HeaderValue
WeSend-EventThe event, for example balance.low.
WeSend-DeliveryThis delivery's id. The same on every retry.
WeSend-Signaturet=<unix seconds>,v1=<hex>. Check it as shown below.

Verify the signature

v1 is the HMAC-SHA256 of t and the raw body joined by a dot (t.body). Check it against the body exactly as received, before parsing it, and refuse a t older than 5 minutes.

import crypto from 'node:crypto'
import express from 'express'

const app = express()

app.post('/wesend/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('WeSend-Signature') || ''
  const { t, v1 } = Object.fromEntries(header.split(',').map(part => part.split('=')))
  const expected = crypto.createHmac('sha256', process.env.WESEND_WEBHOOK_SECRET)
    .update(`${t}.${req.body}`).digest('hex')

  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300
  const valid = v1?.length === expected.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
  if(!fresh || !valid) return res.sendStatus(400)

  const event = JSON.parse(req.body)
  if(event.type === 'balance.low') {
    // Top up, alert the team...
  }
  res.sendStatus(200)
})

Retries

  • A 2xx answer within 10 seconds counts as delivered. Any other answer, a timeout or no connection is retried.
  • Retries come after 1, 5 and 30 minutes, then 2, 6 and 12 hours, 7 tries in all. After that it is marked failed.
  • Redirects are not followed. The address must be a public https server.
  • The last 30 days of deliveries and answers are under Settings, Webhook.

The project's members are also told by email, and its owner by SMS. A webhook adds to those; it does not replace them.