Webhook Receiver Contract and Signature Rotation Verifier avatar

Webhook Receiver Contract and Signature Rotation Verifier

Pricing

from $20.00 / 1,000 run starteds

Go to Apify Store
Webhook Receiver Contract and Signature Rotation Verifier

Webhook Receiver Contract and Signature Rotation Verifier

Sends signed and deliberately bad deliveries to one webhook receiver and reports, for each case, whether the receiver answered with the expected HTTP status. Use it after a deploy and after a secret rotation. HTTP only, no browser, no proxy.

Pricing

from $20.00 / 1,000 run starteds

Rating

0.0

(0)

Developer

kingii98

kingii98

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

8 days ago

Last modified

Categories

Share

Prove that your webhook receiver accepts good deliveries and rejects bad deliveries. Run it after each deploy of the receiver and after each secret rotation.

The Actor sends a small set of deliveries to one endpoint that you own. Each delivery uses a signature mode that you select. The Actor compares the HTTP status of the answer with the status range that you expect, and it reports one pass or fail verdict for each case plus one gate verdict for the run.

A fail verdict is a result, not a malfunction. The run always ends SUCCEEDED, and the verdict is in the dataset and in the run status message. Use the ciGatePass field of the summary record to gate a deploy in CI.

What it does

  1. Reads the receiver URL, the signature scheme and the test cases from the input.
  2. Refuses a target that is not a public HTTPS address. Loopback, private, link-local and reserved addresses are refused, and redirects are never followed. A refused target is a result, not a malfunction: the Actor writes one summary record with status TARGET_REFUSED and the run ends SUCCEEDED.
  3. Sends the cases one after the other, at the configured rate limit.
  4. Optionally sends a tamper and replay probe set of four extra requests for each case.
  5. Writes one dataset record for each case and each probe, plus one summary record.

Signature modes

ModeWhat goes outWhat a correct receiver does
validBody signed with the current secretAccept, 2xx
missingNo signature headerReject
wrong-secretBody signed with a secret that is not yoursReject
previous-secretBody signed with the previous secretReject after a rotation
replayed-timestampCorrect signature over a stale timestampReject, if it checks a window
oversized-bodyA padded body of oversizedBodyBytesReject or accept, your contract

The tamper and replay probe set adds four more requests for each case: tampered-body (a valid signature over a body that then changed), tampered-signature (one character of the digest changed), replayed-timestamp and previous-secret. A probe passes when the receiver answers inside probeExpectedStatusMin to probeExpectedStatusMax, 400 to 499 by default.

A mode that the configuration cannot serve is reported as skipped, never as a fail. Examples: a previous-secret case without previousSecret, or any signature mode when signatureScheme is none.

Input

Every field has a default, so a run with empty input {} works. It sends three cases to a public echo endpoint and shows the record shape.

FieldDefaultMeaning
receiverUrlhttps://postman-echo.com/postOne public HTTPS endpoint that you own
signatureSchemehmac_sha256hmac_sha256, hmac_sha1 or none
signatureHeaderX-Signature-256Header that carries the signature
signatureTemplatesha256={signature}Header value format; {signature}, {signatureBase64}, {timestamp}
signedPayloadTemplate{body}Bytes that are signed; {body}, {timestamp}
timestampHeaderX-TimestampTimestamp header; empty sends none
currentSecretemptySecret that the receiver must accept
previousSecretemptySecret that the receiver must reject after a rotation
cases3 cases1 to 50 cases: name, mode, payload, expected status range
tamperReplayProbesfalseAdd the four-request probe set for each case
probeExpectedStatusMin / Max400 / 499Status range that counts as a correct rejection
timeoutSeconds101 to 15 seconds for one request
maxRequests200Hard cap on requests in one run, 1 to 500
requestsPerSecond5Rate limit for the receiver, 1 to 10
maxResponseBytes65536Cap on the response bytes that are read and hashed
oversizedBodyBytes100000Size of the padded body of the oversized-body mode
replayAgeSeconds900Age of the stale timestamp of the replay modes
contentTypeapplication/jsonContent-Type of every request
userAgentWebhookContractVerifier/0.1 (+https://apify.com)User-Agent of every request

The fields that become an HTTP header name or value (signatureHeader, signatureTemplate, timestampHeader, contentType, userAgent) must hold ASCII characters only, because HTTP headers are ASCII. A value that holds, for example, an en dash is refused with an input error.

A payload can be an object, an array or a string. The tokens {nonce} and {timestamp} in the payload are replaced before the body is signed, so each delivery is unique and a receiver that stores delivery IDs sees no duplicate.

Example: verify a rotation

{
"receiverUrl": "https://api.example.com/webhooks/github",
"signatureScheme": "hmac_sha256",
"signatureHeader": "X-Hub-Signature-256",
"signatureTemplate": "sha256={signature}",
"currentSecret": "the-new-secret",
"previousSecret": "the-rotated-out-secret",
"cases": [
{"name": "new-secret-accepted", "mode": "valid",
"payload": {"zen": "ping", "id": "{nonce}"},
"expectedStatusMin": 200, "expectedStatusMax": 299},
{"name": "old-secret-rejected", "mode": "previous-secret",
"payload": {"zen": "ping", "id": "{nonce}"},
"expectedStatusMin": 401, "expectedStatusMax": 403},
{"name": "no-signature-rejected", "mode": "missing",
"payload": {"zen": "ping", "id": "{nonce}"},
"expectedStatusMin": 401, "expectedStatusMax": 403}
]
}

The default input fixture points at a public echo service that checks no signature. It therefore answers 200 to every case, and the three cases expect 200 to 299. Point receiverUrl at your own endpoint and tighten the expected ranges, as the example above shows.

Output

One dataset record for each case (recordType: "case") and each probe (recordType: "probe"):

FieldMeaning
caseName, signatureModeWhich case, and which signature mode went out
signatureSent, timestampSentWhether a signature header went out, and the timestamp in it
requestBodyBytesSize of the body that went out
httpStatusStatus of the answer, null when there was no answer
expectedStatusThe expected range, for example 401-403
verdictpass, fail, error or skipped
latencyMsTime from the request to the end of the answer
responseBodyHashsha256: of the response bytes that were read
responseBytes, responseTruncatedHow much was read, and whether the cap cut it
error, noteTransport error text, or the reason for a skip

One summary record (recordType: "summary") closes the run: totalPass, totalFail, totalError, totalSkipped, worstLatencyMs, casesExecuted, probeSetsExecuted, requestsSent and ciGatePass.

ciGatePass is true only when there is no fail verdict, no transport error, and at least one case passed.

Pricing

Pay per event:

EventUnitWhen it is charged
run-startedOne Actor runOnce for each run, after the input is read
test-case-executedOne test case sent to the receiverOnce for each case that reached the receiver and returned an HTTP status
tamper-replay-probeOne tamper and replay probe set for one caseOnce for each probe set where at least one probe reached the receiver

A case that is skipped, or that never got an answer from the receiver, is not charged. A run that refuses the target charges only run-started.

Limits and safety

  • HTTP only. No browser, no proxy, no login, no external database.
  • HTTPS targets only. Loopback, private, link-local and reserved addresses are refused.
  • Credentials in the URL are refused.
  • Redirects are never followed. A 3xx answer is reported as it is.
  • Requests are sequential and rate limited, so the Actor cannot act as a load generator.
  • maxRequests caps the requests of one run. Cases beyond the cap are skipped.
  • The response body is read up to maxResponseBytes and then cut.
  • The receiver URL is written to the run log, so every delivery is traceable.

Development

uv sync
uv run pytest
uv run ruff check .