Securing a Webhook Receiver: Sender Authentication, Replay Windows, Secret Rotation and Safe Processing

Developer guides

For: Backend or security engineer who owns the public endpoint that receives verification callbacks

How to protect the endpoint that receives verification callbacks: HMAC and HTTP message signatures, replay windows, secret rotation and the order of checks before work is queued.

A webhook receiver is a public URL that does something when it is called. That one fact explains most of what can go wrong: anyone who learns the address can call it, anything that sees a request in plaintext, such as a compromised endpoint, a TLS terminator, an intermediary or a log, can read or copy it, and a genuine request that was valid yesterday can be sent again today. This guide is about the endpoint itself: how it decides that a request comes from the sender it trusts, how it limits the damage of a copied request, how its secret is replaced without an outage, and in which order it does its checks before any real work is queued.

It adds to two articles that already exist on this site. Receiving verification results by polling or webhook decides how a result should reach your system and builds the reconciliation loop that makes a lost event harmless. The verification session lifecycle lists the four events, the options of the callback and the habits of an idempotent receiver. Here the subject is the security of the request path in front of those habits: authentication of the sender, replay windows, rotation of the shared secret and safe processing of an untrusted body. The general parts rest on the standards for message authentication codes and HTTP message signatures, on the cheat sheets of the Open Worldwide Application Security Project (OWASP), and on the published webhook documentation of two widely used platforms and one community specification.

What the NeuroVision documentation shows, and what it leaves to you

Before any general advice, it is worth separating what the documentation of this platform states from what this article takes from elsewhere. The webhook page of the documentation lists four events and four callback options, which the lifecycle article walks through, so they are not repeated here. For this article two of the options matter. One option encrypts the body of the callback request with the secret key of the scenario. One omits identity fields, so that, for the session event, names, document data and images stay out of the callback and the session identifier stays in. The omission works event by event, and it is not a promise that a callback holds nothing sensitive: the documentation says that each terminal result of a database check, such as AML screening, is sent in full whatever the option, and the lifecycle article says the same of the check results of a questionnaire. Treat a callback with omitted fields as one that can still carry screening results, and protect it as such. The page gives the receiving rule in one sentence: treat the event as a signal and fetch the result by the session identifier through the REST API. It also describes a test request in the callback settings, which you use to confirm that the address is reachable.

The page does not describe a request signature, a signature header, a timestamp field or an identifier of the delivery attempt. It does not state how often an event is sent again or for how long. This article therefore does not claim that the platform offers any of these. Where the sections below describe a signature, a timestamp or a rotation scheme, they describe what other senders and the standards do, so that you know what to look for and what to ask. If a mechanism of this kind exists for your account, it will be in the provider documentation or in the answer to your question to the team, and that source decides what your code must verify.

What the platform does give you is a design that lowers the stakes of a forged request. Because the event is a signal and the result is fetched over your own authenticated call, a forged event cannot put a verdict into your system by itself. Because the encryption option exists, the body of a genuine event can be made unreadable to a party on the path. Neither of these replaces the checks below, and the section on authentic events explains why.

Threats to a receiving endpoint

Six threats are worth naming, because each one is answered by a different control.

ThreatWhat the attacker doesControl that answers it
ForgeryPosts a body that claims to come from the senderVerify a message authentication code or a digital signature over the body
TamperingChanges a genuine body on the pathThe same signature, plus HTTPS on the endpoint
ReplaySends a genuine, signed request again, laterA timestamp inside the signed content and a memory of identifiers
FloodingSends many requests, each forcing workCheap checks first, size limits, throttling, work done after the answer
Hostile contentSends a body built to break the parser or the logicStrict parsing, schema validation, a content type check
Secret exposureReads the shared secret from a repository, a log or an old laptopSecret storage, rotation and a separate secret for each endpoint

The OWASP cheat sheet on REST security states the baseline for the transport. Secure REST services must only provide HTTPS endpoints, which protects credentials in transit, lets clients authenticate the service and guarantees the integrity of the transmitted data. A webhook endpoint is a REST service in this sense, and the platform you receive from is its client. So the endpoint serves HTTPS only, and a plain HTTP listener, even one that redirects, is not part of the design.

Authenticating the sender

A shared secret and a keyed hash

The most common way to prove that a request comes from the sender is a message authentication code computed with a secret that only the two parties know. The community specification called Standard Webhooks describes the same approach as the most common one, with HMAC signatures made with a pre-shared secret key, and names asymmetric signatures as the usual alternative. HMAC is defined in RFC 2104, published in February 1997 by Krawczyk, Bellare and Canetti as an informational document. Its abstract describes a mechanism for message authentication that uses a cryptographic hash function in combination with a secret shared key. The computation is H(K XOR opad, H(K XOR ipad, text)), where H is the hash function, K the key, and ipad and opad two fixed byte strings. You do not implement this yourself: every mainstream language has an HMAC function, and your task is to feed it the right bytes and to compare the result in the right way.

RFC 2104 also says three things about the key that apply directly to a webhook secret. Keys need to be chosen at random, or by a cryptographically strong generator seeded with a random seed, and periodically refreshed. A key shorter than the length of the hash output is strongly discouraged, because it decreases the security strength of the function. The document is explicit that current attacks do not indicate a specific recommended frequency for changing keys, but that periodic refreshment is a fundamental security practice that limits the damage of an exposed key. Both parts of that sentence matter: no emergency forces a schedule, and a schedule is still worth having. The RFC also discusses truncating the output of the code and recommends that the output length be not less than half the length of the hash output and not less than 80 bits. A receiver should therefore not accept a signature that has been shortened below what the specification of its sender documents.

What bytes are signed

A signature is only as good as the question of what it covers. Three published schemes give three answers, and the differences are instructive.

The GitHub documentation says that the hash signature is generated with the secret token and the payload contents, that it arrives as the value of the X-Hub-Signature-256 header, that it is an HMAC hex digest, and that it always starts with sha256=. It also says that if your language specifies a character encoding, the payload must be handled as UTF-8. The Stripe documentation says that every event is signed with a signature in the Stripe-Signature header, and that the signature is computed over the raw body of the request together with a timestamp that is part of the signed payload. The Standard Webhooks specification signs three things joined with full stops: the identifier of the message, the timestamp and the body, in the form msg_id.timestamp.payload. It states why: a secure signature scheme should verify the authenticity of the payload and of the additional metadata, namely the timestamp of the attempt and the unique identifier of the webhook.

Two practical rules follow from all three.

Verify the raw bytes, not a re-serialised object. Stripe states that it requires the raw body of the request for signature verification, and that any manipulation of the raw body by a framework makes verification fail. The Standard Webhooks specification names the usual cause: many consumers parse the body as JSON and then serialise it again, and even a stray space makes the signature invalid. In an Express-style framework this means that the route which receives the callback must read the body as a buffer before any JSON middleware touches it. In a framework that gives you only the parsed object, you cannot verify the signature correctly, and you should change the route.

Check the signature before you parse, branch or log the content. The GitHub documentation gives the reason: validating the signature before processing the delivery avoids spending server time on deliveries that are not from the sender and helps avoid man-in-the-middle attacks. A forged body should cost you one hash computation, a comparison and a rejected request, and nothing more.

Comparing in constant time

Comparing the computed code with the received one by an ordinary equality operator can leak, through timing, how many bytes at the start matched. The GitHub documentation tells the reader never to use a plain == operator and to use a method such as secure_compare or crypto.timingSafeEqual, which compares in constant time. The Standard Webhooks specification adds that failing to do so can expose consumers to timing attacks and turn them into signing oracles. The Node.js documentation describes crypto.timingSafeEqual as a constant-time comparison that does not leak timing information that would allow an attacker to guess one of the values, suitable for comparing HMAC digests. It also states that both arguments must have the same byte length, otherwise an error is thrown, so the length check comes first and is not secret.

An illustrative verifier

The following sketch uses the header names of the Standard Webhooks specification only as an example of a scheme that signs an identifier, a timestamp and a body. They are not the names of any header sent by NeuroVision, because the documentation of the platform shows no such header. Replace them with what your sender documents.

import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the Buffer exactly as received. secret: a Buffer from your secret store.
// Illustrative header names from a community specification; use your sender's own.
export function verify(rawBody, headers, secrets, toleranceSeconds = 300) {
  // A zero or negative tolerance would disable the recency check: refuse to start.
  if (!Number.isInteger(toleranceSeconds) || toleranceSeconds <= 0) {
    throw new Error("toleranceSeconds must be a positive integer");
  }
  const id = headers["webhook-id"];
  const tsText = String(headers["webhook-timestamp"] || "");
  const header = headers["webhook-signature"];
  if (!id || !/^[0-9]+$/.test(tsText) || typeof header !== "string") return "reject";
  const ts = Number(tsText);

  const age = Math.abs(Math.floor(Date.now() / 1000) - ts);   // future timestamps count too
  if (age > toleranceSeconds) return "stale";

  // Each component is "version,base64". Accept only the version this verifier supports.
  const candidates = [];
  for (const component of header.split(" ")) {
    const parts = component.split(",");
    if (parts.length !== 2 || parts[0] === "" || parts[1] === "") return "reject"; // malformed
    if (parts[0] === "v1") candidates.push(parts[1]);   // keyed hash; other versions are skipped
  }
  if (candidates.length === 0) return "reject";

  const signedContent = Buffer.concat([Buffer.from(`${id}.${ts}.`), rawBody]);
  for (const secret of secrets) {                 // more than one secret during a rotation
    const expected = Buffer.from(
      createHmac("sha256", secret).update(signedContent).digest("base64"));
    for (const value of candidates) {
      const received = Buffer.from(value);
      if (received.length === expected.length && timingSafeEqual(received, expected)) {
        return "ok";
      }
    }
  }
  return "reject";
}

The function does its work in a fixed order: it refuses a tolerance that would disable the recency check, checks that the metadata exists and is well formed, rejects a timestamp outside the window in either direction, reads the version of each signature component, builds the signed content from the raw bytes, tries every secret that is currently valid, and compares in constant time. The Standard Webhooks specification distinguishes a version for keyed-hash signatures from a version for asymmetric ones, so the verifier accepts only the version it implements and does not guess at the others. A malformed component rejects the request, and a request with no component it supports is rejected too. It does not parse the body and it logs nothing. Everything that follows in the handler happens only after it returns ok.

Asymmetric signatures and HTTP message signatures

A shared secret has a structural weakness: both sides hold it, so the receiver's copy is also a signing key. The Standard Webhooks specification says that if you do not control the security of both the producer and the consumer, an asymmetric signature is recommended, and it defines Ed25519 as the asymmetric option. With an asymmetric scheme only the producer holds the private key, and the consumer verifies with a public key that is not secret. The same document warns that a consumer should keep a trust list of public keys and signature schemes, and should not trust a public key read from a header of the request itself. A key taken from the request that it is supposed to authenticate authenticates nothing.

RFC 9421, HTTP Message Signatures, is a Standards Track document of the Internet Engineering Task Force published in February 2024. Its abstract describes a mechanism for creating, encoding and verifying digital signatures or message authentication codes over components of an HTTP message. Where the vendor schemes above sign "the body plus a timestamp", RFC 9421 lets the signer choose which components of the message are covered, and the signature carries parameters that describe it. The parameters include created, the creation time as a UNIX timestamp, whose inclusion is recommended, expires, nonce, a random unique value for this signature, alg and keyid. The registry of signature algorithms includes hmac-sha256, HMAC using SHA-256, and ecdsa-p256-sha256. For a webhook receiver the value of the standard is in what it says about coverage and replay, which applies to any scheme: a signature over too little of the message can be replayed at will, and the extreme case is a signature over no components at all, which could be attached to any message. The RFC advises that an application require as much of the message as possible to be signed, and that the verifier trust only the components that were signed. If your sender uses RFC 9421, your verifier checks the created and expires values and the covered components before it trusts any header or the body, and it treats an unsigned component as untrusted input.

IP addresses are a second layer

Some senders publish the addresses from which they send events. Stripe recommends using both protections together: restricting the endpoint to the published addresses, and verifying the signature. An address filter lowers the noise from random scanners, but it is not authentication: addresses of cloud providers are shared, published lists change, and a proxy in front of your service can hide the real source. Treat the filter as a layer in front of the signature, never in place of it.

Replay windows

A signature proves who made a request and that the content is unchanged. It does not prove when the request was made, and a captured request with its signature still verifies tomorrow. A replay is cheap for the attacker: no key, no analysis, only a copy.

Timestamp tolerance

The usual answer is a timestamp inside the signed content and a tolerance on the receiver. Stripe describes a replay attack as an attacker intercepting a valid payload and its signature and re-transmitting them. It includes a timestamp in the signature header, so that the timestamp, being part of the signed payload, cannot be changed without invalidating the signature, and the application can reject a payload whose timestamp is too old. Its libraries have a default tolerance of 5 minutes between the timestamp and the current time. Stripe warns against a tolerance value of 0, because it disables the recency check entirely, and it tells the reader to use the Network Time Protocol so that the server clock agrees with the sender. The Standard Webhooks specification gives the same rule in its own words: verify that the timestamp header is within some allowable tolerance of the current timestamp to prevent replay attacks.

Choosing the window is a trade of three quantities. The window must be longer than the sum of your clock error, the network delay and the time a sender may hold a delivery before it sends it. It should be as short as that allows, because every second of window is a second in which a copy is accepted. And it must be shorter than the time for which you remember identifiers, which is the next control. A tolerance of a few minutes is common in published material; the number you choose is yours, and a wide window is a reason to invest in the identifier memory.

A retry changes the timestamp. Stripe states that it generates the timestamp and signature each time it sends an event, so a retry after a failed delivery carries a new timestamp and a new signature. The Standard Webhooks specification makes the same distinction: the timestamp of the attempt is updated on every retry, while the identifier of the message stays the same and the timestamp of the original event does not change. So the timestamp of a request limits replay in time, and it cannot tell you whether you have already processed the event. That is the job of the identifier.

Remembering identifiers

The Standard Webhooks specification recommends using the identifier header as an idempotency key, so that the same webhook is never processed twice, and gives as an example saving the identifiers in a cache for 5 minutes. Stripe tells the receiver to log the identifiers of the events it has processed and not to use the creation time to decide whether an event is a duplicate, because the creation time has a resolution of seconds and distinct events can share it. A table of seen identifiers gives you replay protection with bounded storage, provided each entry is kept for as long as a copy could still pass the clock. The verifier above compares the absolute difference between the clock and the timestamp, so it also accepts a timestamp that lies in the future. A copy dated in the future stays valid until its signed timestamp plus the tolerance, and an entry that expires a fixed time after first receipt can expire earlier than that. So keep each receipt at least until the signed timestamp plus the tolerance, and either reject timestamps that lie too far ahead or keep their receipts longer. This replay memory is a separate matter from business deduplication: the sender may deliver the same message again, with a new timestamp, over a retry horizon that the sender defines, and your own downstream guard must hold for that whole horizon. A table that answers "seen this signed request" is not a substitute for a record of "processed this event". The durable record of the identifier belongs in the same transaction as the job it protects, with a uniqueness constraint on the identifier, as the order of checks below explains. A fast cache in front of that store is safe only if it never claims more than the store knows: fill it after the transaction commits, or treat a cache entry as a short lease, so that a copy which meets an unexpired lease gets a retriable answer or a check against the durable record, never a silent "already done". Otherwise a cache entry written just before a failed or interrupted transaction would make the next delivery look like a duplicate, and the work would be lost.

When the sender gives no timestamp

If the documentation of your sender shows no timestamp and no stable identifier, you cannot enforce a replay window, and you should say so in your threat model instead of pretending. What remains is to make a replay harmless. The pattern recommended on the NeuroVision webhook page does exactly that: the event is a signal, the handler fetches the current state by session identifier, and a repeated event leads to a repeated read, not to a repeated action. For actions that must not be repeated, such as a payment, a message or a block, the guard belongs downstream, keyed on the session identifier and the outcome, as the lifecycle article describes. The platform documentation also notes for the database check event that it can arrive again with the full result set, so the receiver treats it as idempotent by session identifier.

ControlStopsDoes not stop
Signature over the raw bodyForgery and tamperingA copy of a genuine request
Timestamp toleranceA copy sent after the windowA copy sent inside the window
Table of seen identifiersA copy inside the window, and a duplicate from the senderA first-time forged request, if the signature is missing
Fetch the state by identifierA forged event that carries an invented verdictA flood of forged events that each trigger a fetch
Action guard downstreamA repeated payment, message or blockNothing upstream

Rotating the secret

Why rotate, and what the sources say

RFC 2104 recommends periodic refreshment of keys as a fundamental practice that limits the damage of an exposed key. The OWASP cheat sheet on secrets management gives the practical reasons: regular rotation means that stolen credentials work only for a short time, secrets should be created to expire after a defined time where possible, and secrets should be revoked when they are no longer required or are potentially compromised. It also notes that rotation done by hand is challenging and can lead to mistakes, and that it is better to automate it or at least to support it well. The sheet lists strategies for rotating keys; one is gradual rotation, in which new keys are introduced for write operations while old keys stay in use for read operations. For a webhook, the sender writes signatures and you read them, so the same idea says: the sender starts to use the new secret, and you keep accepting the old one for a while.

How senders support an overlap

Two published designs show what an overlap looks like from the sender's side. The Standard Webhooks specification sends a list of signatures separated by spaces in one header, so that a webhook can be signed both with the current key and with an old key for a set period, and the consumer tries each signature until one matches. It gives the reason: the secret should not change under normal circumstances, but it may have to change, for example on a compromise, and zero-downtime rotation means that operations are not affected. It adds that signing with several keys, even a compromised one, does not weaken the scheme, because the consumer still needs a valid signature. Stripe lets the owner of an endpoint roll its signing secret, choosing to expire the current secret at once or to delay its expiry for up to 24 hours, and during that time it generates one signature for each active secret. Both designs put the choice of the overlap in the hands of the receiving team, which is why the verifier above takes a list of secrets.

A rotation procedure

A rotation that has been practised is safer than a plan on paper. The steps below assume that your sender supports an overlap, or that you can accept two secrets for the same endpoint; if it does not, you need a maintenance window and a way to replay missed events, which the reconciliation loop of the polling article provides.

  1. Generate the new secret in your secret store, with the length and randomness the sender specifies.
  2. Deploy a receiver that accepts both the old and the new secret.
  3. Switch the sender to the new secret, with an overlap if the sender offers one.
  4. Watch the verifier: you should see signatures that match the new secret, then none that match only the old one.
  5. Remove the old secret from the receiver and revoke it in the store.
  6. Record the date, the reason and who did it, so that the next rotation starts from facts.

If the reason for the rotation is a suspected leak, the order changes. Rotate at once without an overlap, accept the failed deliveries that follow, and recover the missed results by reading the sessions that your own table still shows as waiting. Then review what the endpoint accepted in the period when the secret might have been known to someone else, because a forged event in that period would have passed the signature check. This review is the reason to log the identifier of each accepted event with the time and the outcome of the check, and nothing of the content.

One secret for each endpoint and environment

The Standard Webhooks specification states that signing keys should be unique for each endpoint, because reusing keys across customers can lead to security issues. For a receiver, the same rule means a separate secret for the test and the production endpoint, for each region or service that receives events, and for each scenario if the platform issues one for each scenario. The GitHub documentation adds the handling rules: choose a random string with high entropy, store it in a secure location that your server can access, and never hardcode it into an application or push it to a repository. A secret that sits in a repository is one rotation away from a leak you cannot see.

Safe processing of a hostile body

The order of checks

The order in which a handler does things matters as much as the checks themselves. Cheap and decisive checks come first, expensive and risky work comes last, and the answer to the sender is sent when the event is safely stored.

  1. Transport. The request arrived over HTTPS.
  2. Method and content type. Accept only the method the sender uses, and the media type it documents.
  3. Size. Reject a body above the limit you set before you read it all.
  4. Raw read. Read the body as bytes, without parsing.
  5. Authenticity. Verify the signature in constant time over the raw bytes.
  6. Freshness. Check the timestamp against the tolerance.
  7. Duplicate. Look the identifier up in the table of receipts.
  8. Parse and validate. Parse the body with a strict parser and check it against the schema you expect.
  9. Store receipt and job together. Write the identifier and the work to the durable store in one transaction. If the transaction fails, answer with an error so that the sender delivers the event again. A receipt that is committed without its job would make the retry look like a duplicate, and the event would be lost without ever being processed.
  10. Answer. Return a success code.

The OWASP cheat sheet on REST security supports steps two, three and eight. It tells the service to allow only an agreed list of HTTP methods and to answer others with 405, to require a supported content type and answer an unsupported media type with 415, and to define an appropriate request size limit and answer a request above it with 413. It tells the service to validate input by length, range, format and type, to reject unexpected content, and to use a secure parser.

Answering the sender

What the receiver answers decides what the sender does next, so the status code is part of the design. RFC 9110 defines the semantics of the codes that a receiver uses. Stripe tells the receiver to return a success code before any complex logic that might cause a timeout; for example, it must return a response before it updates an invoice in an accounting system. The Standard Webhooks specification considers a delivery successful when the response has a status in the 2xx range and a failure in every other case, including redirects, errors, request timeouts and connection resets. It recommends a request timeout for webhooks between 15 and 30 seconds, which tells you how long your handler may take before the sender gives up and tries again.

SituationAnswerReason
The event is storedA success code, quicklyThe sender stops retrying
The store is unavailableA server error code, not successThe sender keeps the event and tries again
The signature is missing or wrongAn authentication or authorisation error, and no processingA forged request must not look accepted
The timestamp is outside the windowThe same class of errorThe sender will send a fresh attempt if it was genuine
The body is too large or of the wrong typeThe codes 413 and 415The request is outside the contract
The event type is one you do not useA success code, and ignore itA refusal would only cause retries

Two traps are worth a sentence each. The Standard Webhooks specification says that a 410 response is read by a sender as a sign that the receiver no longer wants webhooks, after which the sender should disable the endpoint and stop sending. So never return 410 from a catch-all route for a path that is merely misconfigured. And a redirect counts as a failure under the same specification, because following redirects loads both sides, which is why the callback URL you register should be the final one.

Do not trust an authentic event further than its content

An event that passes every check above is authentic, which is a statement about its origin and not about the world. The sender may deliver an event about a session that your system never created, a mistaken configuration may point a test scenario at the production endpoint, and a signed event can still describe something your application should not act on. The lifecycle article therefore asks the handler to match the identifier to a session that your own system created, for the applicant and the workflow it expects, and to ignore any other. The OWASP list of API risks makes the same point for the first of its risks, broken object level authorisation: an identifier supplied from outside is a wide attack surface, and the check belongs in every function that uses it. Authenticity answers "who sent this"; the match to your own record answers "is this ours".

Flooding and the cost of a rejected request

The OWASP API list also names unrestricted resource consumption as a risk, because serving requests uses network, processor and memory. The cheap-first order above is the answer on your side. A request that fails the size check, the content type check or the signature check should never reach the database, a queue or a call to the platform. The OWASP cheat sheet suggests answering a flood with 429 when requests come in too quickly, and recommends logging input validation failures, with the observation that a source that causes hundreds of failures each second is up to no good. Throttle in front of the application where you can, at the proxy or the gateway, so that the application does not spend a thread on each rejected request.

Logging without leaking

The log of a webhook endpoint is a second copy of whatever you put into it. Log the time, the event name, the identifier, the outcome of each check and the decision, and not the body, the signature or the decrypted content. The OWASP cheat sheet asks for audit logs before and after security-related events, for logging of token validation errors, and for sanitising log data to prevent log injection. The same cheat sheet warns that credentials, tokens and keys must not appear in a URL, because a URL can be captured in web server logs. For a callback this means that the secret is never a path segment or a query parameter of the address you register. Where the callback body is encrypted with the secret key of the scenario, decrypt it inside the handler after the checks and keep the plaintext out of log lines, error reports and traces.

A test plan for the receiver

Each control above needs a test that fails when the control is removed. The cases below can be run against a staging endpoint with a script that you write, using a secret that exists only for the test.

  1. Good request. A correctly signed, fresh, first-time request is accepted and the job is stored.
  2. Wrong secret. The same request signed with another secret is rejected, nothing is parsed and nothing is queued.
  3. Changed byte. One byte of the body is altered after signing; the request is rejected.
  4. Re-serialised body. The body is parsed and serialised again before the check, to prove that your route verifies raw bytes; it must fail the signature if the bytes differ.
  5. Old timestamp. A request just outside the tolerance is rejected, and one just inside is accepted.
  6. Replay. The same accepted request is sent twice inside the window; the second is recognised as a duplicate.
  7. Zero tolerance. A configuration that disables the freshness check is refused at start-up.
  8. Rotation. During an overlap both secrets verify; after the old one is removed, only the new one does.
  9. Wrong method, type and size. The three requests get the codes the contract says, and none of them reaches the verifier.
  10. Slow store. The job store is made unavailable; the handler answers with an error and not with success.
  11. Log review. After all of the above, search the logs for the secret, the signature and a fragment of a body.

Where this sits in a verification flow

The REST API, the dashboard and the webhooks are the three parts of the platform that this endpoint design touches. Webhooks tell your backend when a session, a questionnaire, a database check or an erasure completes, and the callback can encrypt its body or omit names, document data and images. Which authentication fields a callback carries for your account is a question for the integration team, and the answer decides what your receiver verifies. For the events and their options, read receiving webhooks; to talk about your own flow, contact the team.

Sources

All sources were fetched on 28 October 2026, except RFC 9110, the OWASP REST cheat sheet and the OWASP API list, fetched on 27 October 2026.

Ready to talk?

Tell us what you are building and we will help you plan the integration.

Talk to us