Requirements in Masnet
Before configuring the receiving system, prepare the destination in Masnet. The steps are the same for every system.
- Open Company › Integrations (the Spanish interface labels it Empresa › Integraciones), go to the Destinations tab and select New destination. If the integration has no authorized data yet, Masnet asks you to authorize it first.
- In Destination address (https) enter the receiver public URL. It must be
https, port 443 or 8443 and a public DNS name: user information in the URL, fragments, IP addresses and internal names are not accepted. A query string is allowed; Masnet treats the whole URL as a credential. - In Format choose Standard JSON (
application/json) or Structured CloudEvents (application/cloudevents+json). The event content is the same; see Webhooks. - Select Create destination. Masnet shows the signing secret (
whsec_…) only once. Store it immediately in the receiver secret store; it cannot be read again. - Select Test destination and then Send test. The integration is activated only after a successful test; then use Activate destination.
Data the verifier needs
- The destination signing secret (
whsec_…), stored outside the code. - The exact body received, uninterpreted. In Express use
express.raw({ type: '*/*' })on the webhook route, so thatreq.bodyis aBuffer; do not useexpress.json()before verifying. - The
webhook-id,webhook-timestampandwebhook-signatureheaders. - A clock tolerance of 5 minutes, which is what the Standard Webhooks libraries apply and what is recommended for the receiver.
The signature is v1, followed by base64(HMAC-SHA256) computed with the binary key obtained by base64-decoding what follows whsec_, over the bytes webhook-id + . + webhook-timestamp + . + the exact body. Always verify over the received bytes, before parsing the JSON.
With the standardwebhooks library
Install standardwebhooks from npm. The library validates the 5-minute tolerance and multiple signatures.
// npm install standardwebhooks
import { Webhook } from 'standardwebhooks'
/**
* `rawBody` is the unparsed request body (string or Buffer); `headers` holds webhook-id,
* webhook-timestamp and webhook-signature. The library enforces a 5 minute tolerance.
*/
export function verify({ secret, headers, rawBody }) {
try {
new Webhook(secret).verify(rawBody, headers)
return true
} catch {
return false
}
}Without dependencies
The function compares in constant time and accepts any of the signatures in the header.
import { createHmac, timingSafeEqual } from 'node:crypto'
/**
* Verifies a Masnet webhook with node:crypto only.
* `rawBody` must be the exact bytes received (Buffer or string), before any JSON parsing.
*/
export function verify({ secret, headers, rawBody, now = Date.now() / 1000, tolerance = 300 }) {
const id = headers['webhook-id']
const timestamp = headers['webhook-timestamp']
const signatures = headers['webhook-signature']
if (!id || !signatures || !/^\d+$/.test(timestamp ?? '')) return false
if (Math.abs(now - Number(timestamp)) > tolerance) return false
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64')
const expected = createHmac('sha256', key).update(`${id}.${timestamp}.`).update(rawBody).digest()
// During a rotation the header carries two signatures separated by a space; any match is valid.
return signatures.split(' ').some((part) => {
const [version, value] = part.split(',')
if (version !== 'v1' || !value) return false
const given = Buffer.from(value, 'base64')
return given.length === expected.length && timingSafeEqual(given, expected)
})
}Operation: duplicates and ordering
- Idempotency.
webhook-ididentifies the event and is kept across retries and redeliveries. Record the identifiers already processed and discard repeats with a 2xx response. - Ordering. Each resource (
subject) carries an increasingsequence. Discard an event whosesequenceis lower than or equal to the last one applied for thatsubject. Events of different resources have no order between them. - Retries. Masnet retries any non-2xx response for up to 72 hours; the full schedule is in Webhooks.
- Secret rotation. When rotating with overlap, the previous secret stays valid for 24 hours and
webhook-signaturecarries twov1,…signatures separated by a space. Accept the event if either one matches.
Troubleshooting
Masnet records the response code of each attempt in the delivery history. These are the effects of each receiver response:
| Receiver response | Effect in Masnet |
|---|---|
| 2xx | Successful delivery. It means receipt, not business processing. |
| 3xx | The redirect is not followed. It counts as a failure and is retried; the history shows the Location without its query string. |
| 400, 401, 403, 404 and other 4xx or 5xx | Retryable failure for up to 72 hours from the start of the delivery cycle. |
| 410 | Masnet disables the destination and cancels pending deliveries. Return 410 only if the destination must stop receiving events. |
| 413 | The event is marked as undeliverable to that destination and is not retried. |
429 or 503 with Retry-After | Retry-After (seconds or HTTP date) is honored up to a maximum of 1 hour. |
| No response within 15 seconds | Timeout (5 s to connect, 15 s in total). It is a retryable failure: answer 2xx and process asynchronously. |
| Verification fails only in production | Almost always the body was reserialized by a middleware. Verify with the original Buffer. |