Webhook secure intake
The production ingest path for every incoming webhook: verify the provider signature (GitHub / Stripe / Shopify / Slack, constant-time, replay-window enforced), schema-validate the now-trusted body against the provider envelope, fingerprint the raw bytes for redelivery dedup, normalize the event timestamp to UTC + epoch, and redact PII before anything hits a log. Five pure-CPU tools - the accept-or-reject gate, run on every event.
5 tools run server-side in one request. You pay once, settle once, and get a single response - no orchestration, no per-step payments, and a partial-success envelope if any step fails. USDC over x402 on any supported chain.
When to use this pack
This pack is the gate an agent executes on EVERY incoming webhook in production. Step 1 is the security decision (reject on an invalid signature; Stripe and Slack timestamps get replay-window enforcement), and only then is the body treated as trusted: an envelope schema check catches provider API-version drift before it breaks your handler three layers down, a sha256 content fingerprint gives you the dedup key for provider redeliveries (or an Idempotency-Key for downstream calls), the event time is normalized to UTC ISO + epoch for storage, and PII is redacted so the audit log stays clean. One deterministic pass from raw bytes to a storable, loggable, deduplicated event.
Tools in this pack
All 5 run inside the single $0.006 call above. Each is also callable on its own if you only need one part.
- Webhook signature verify POST /api/webhook-verify Verify a webhook's HMAC signature against the correct per-provider scheme: GitHub (X-Hub-Signature-256, sha256=hex), Stripe (Stripe-Signature t/v1 over "<t>.<body>" with replay tolerance), Shopify (X-Shopify-Hmac-Sha256, base64), Slack (X-Slack-Signature, v0:<ts>:<body> with replay tolerance). Constant-time comparison; the secret is never echoed. Pass the RAW request body string - signatures are over the raw bytes. Deterministic.
- JSON Schema validate POST /api/json-validate Validate a JSON document against a JSON Schema (draft-07 subset) and get the list of violations. Supports type, required, properties, items, enum, const, min/max, length, pattern, format, anyOf/allOf/oneOf/not, additionalProperties. Deterministic - check an agent's structured output before you trust it.
- Hash POST /api/hash Cryptographic hash of a text string (UTF-8): returns algo, hex and base64 digests. Algorithms: sha256 (default), sha512, sha1, md5. Up to 100KB of text; for every digest (plus CRC32) in one call use checksum.
- Time convert POST /api/time-convert Convert between epoch (s or ms), ISO 8601, and any IANA timezone. Give a value (epoch seconds or milliseconds, an ISO string, or 'now'), get every representation back: utc, epochSeconds, epochMillis and, with tz, timezone, localIso (ISO 8601 with the zone's offset, e.g. 2026-06-11T19:00:00+09:00), utcOffset and local (a display string). A bare number under 1e12 is read as seconds, otherwise as milliseconds.
- Redact PII POST /api/redact Mask emails, phone numbers, credit-card-like numbers, IPs, and SSNs in text. Returns the redacted text and counts by type.
Bought one at a time, these 5 tools cost $0.006 together; the pack is that sum less a 10% bundle discount, rounded up to the $0.001 settlement floor, which is $0.006.
Workflow
- Verify the signature with webhook-verify - the accept/reject gate, and the reason this pack exists. Pass provider ('github' | 'stripe' | 'shopify' | 'slack'), the RAW body string exactly as received (signatures are over the raw bytes - a parsed-then-restringified body will not match), the signing secret, and the signature header value (scheme prefixes like 'sha256=' / 'v0=' / 't=…,v1=…' are handled). The tool recomputes the correct per-provider HMAC scheme and compares in constant time; for Stripe and Slack it also enforces the ±300s replay window when the timestamp is supplied (Stripe's can ride inside the signature's t= element). If valid=false: stop, return 401 to the sender, and log nothing but the fingerprint from step 3 - an unverified body is attacker-controlled input.
- Schema-validate the now-trusted body with json-validate against a minimal provider envelope schema (GitHub push events carry repository/ref; Stripe events carry id/type/created; Slack carries type/event_time). The signature proves the bytes came from the provider - the schema proves they're the SHAPE your handler was written against. A schema failure on a validly-signed body is the API-version-drift signal (providers add, rename, and sunset fields on a schedule): alert loudly and route to a quarantine queue instead of silently dropping fields. Swap in your own event-specific schema per webhook topic in production.
- Fingerprint the raw bytes with hash (sha256). Providers redeliver on timeout and at-least-once delivery is the contract - the same event can arrive three times with three different delivery ids. The content hash is the dedup key that survives redelivery: check it against your processed-events store before doing any work, or reuse it directly as the Idempotency-Key header on downstream paid calls. Hashing the RAW body (not the parsed object) keeps the fingerprint byte-stable and lets you log it safely even for rejected events - it reveals nothing about the payload.
- Normalize the event timestamp with time-convert. Every provider ships a different clock format: Stripe created is epoch seconds, Slack event_time is epoch seconds, Shopify created_at is RFC 3339 with offset, GitHub commits carry ISO 8601. time-convert returns UTC ISO + epochSeconds + epochMillis in one call - store the epoch (sortable, timezone-proof), render the ISO. This is also where event-ordering bugs die: comparing a provider's local-offset string against your stored UTC string sorts wrong twice a year; comparing epochs never does.
- Redact the body with redact before it touches any log or trace. Webhook payloads routinely carry emails, IPs, phone numbers, and card fragments; the redacted string replaces each with a typed placeholder and the response counts each kind so you can emit 'redacted 1 email, 0 cards' as a metric. Persist ONLY the redacted string in logs - the redact-before-log ordering is the difference between a clean audit and a reportable incident. (The full raw body, if you must keep it, belongs encrypted in your event store keyed by the step-3 fingerprint, not in the log pipeline.)
Arguments
| Name | Required | Description | Example |
|---|---|---|---|
rawBody | yes | the raw webhook body exactly as received on the wire (signatures are over the raw bytes) | {"ref":"refs/heads/main","before":"6113728f27ae82c7b1a177c8d03f9e96e0adf246","after":"d6fde92930d4715a2b49857d24b940956b26d2d3","repository":{"full_name":"acme/checkout-service"},"pusher":{"name":"alice","email":"alice@example.com"},"head_commit":{"id":"d6fde92930d4715a2b49857d24b940956b26d2d3","message":"fix: retry payment capture on 5xx","timestamp":"2026-07-01T15:04:05Z"}} |
provider | yes | which provider signed the webhook: github | stripe | shopify | slack | github |
secret | yes | the webhook signing secret from the provider dashboard (never echoed back) | gh_hook_secret_demo_only |
signature | yes | the signature header value, with or without its scheme prefix (sha256= / v0= / t=...,v1=...) | sha256=45f74caa8f537323fd4fa022357ebc620cbcfb28a6dcd65b0f1da3646edf5c4a |
timestamp | no | the provider timestamp header - required for stripe/slack replay protection (stripe's may ride in the signature's t= element) |
What one call returns
A JSON object with pack, args, steps, summary; steps holds one entry per tool (webhook-verify, json-validate, hash, time-convert, redact), each with its own result or error. Full example on the API page.
Call it directly
Any x402 client pays the 402 and gets the whole workflow back in one response. With the agent402-client SDK (npm i agent402-client, an ES module):
import { Agent402 } from "agent402-client";
// payFetch: an x402-wrapped fetch your wallet signs (@x402/fetch).
// Tools on the free tier need no options: new Agent402() pays them by proof-of-work.
// an existing prepaid credits key also works: new Agent402({ creditsKey })
const client = new Agent402({ fetch: payFetch });
const result = await client.call("skill-webhook-intake", {"rawBody":"{\"ref\":\"refs/heads/main\",\"before\":\"6113728f27ae82c7b1a177c8d03f9e96e0adf246\",\"after\":\"d6fde92930d4715a2b49857d24b940956b26d2d3\",\"repository\":{\"full_name\":\"acme/checkout-service\"},\"pusher\":{\"name\":\"alice\",\"email\":\"alice@example.com\"},\"head_commit\":{\"id\":\"d6fde92930d4715a2b49857d24b940956b26d2d3\",\"message\":\"fix: retry payment capture on 5xx\",\"timestamp\":\"2026-07-01T15:04:05Z\"}}","provider":"github","secret":"gh_hook_secret_demo_only","signature":"sha256=45f74caa8f537323fd4fa022357ebc620cbcfb28a6dcd65b0f1da3646edf5c4a","timestamp":"..."});
Run it in Claude
claude mcp add agent402 -s user -- npx -y agent402-mcp@latest
Then paste this prompt into Claude:
Securely ingest this incoming webhook using Agent402's webhook-intake skill pack.
Provider: github
Raw body (byte-for-byte as received on the wire - never re-serialize before verifying):
{"ref":"refs/heads/main","before":"6113728f27ae82c7b1a177c8d03f9e96e0adf246","after":"d6fde92930d4715a2b49857d24b940956b26d2d3","repository":{"full_name":"acme/checkout-service"},"pusher":{"name":"alice","email":"alice@example.com"},"head_commit":{"id":"d6fde92930d4715a2b49857d24b940956b26d2d3","message":"fix: retry payment capture on 5xx","timestamp":"2026-07-01T15:04:05Z"}}
Signature header value: sha256=45f74caa8f537323fd4fa022357ebc620cbcfb28a6dcd65b0f1da3646edf5c4a
Signing secret: gh_hook_secret_demo_only
(1) webhook-verify with provider, payload=the RAW body string, secret, signature (scheme prefix ok). This is the accept/reject gate: if valid=false, STOP - return 401 to the sender and log only the step-3 fingerprint, never the body. For stripe/slack also pass the provider timestamp so the ±300s replay window is enforced (stripe's t= element inside the signature works too). (2) json-validate the parsed body against the provider envelope schema - for GitHub push events {type:'object', required:['repository']}; for Stripe require id/type/created. A schema failure on a VALID signature = provider API-version drift: alert loudly, quarantine, don't silently drop fields. (3) hash the raw body with algo='sha256' - the content fingerprint. Dedup redeliveries on it (at-least-once delivery means the same event arrives more than once) or reuse it as the Idempotency-Key for downstream calls. (4) time-convert the event timestamp (GitHub head_commit.timestamp ISO; Stripe created / Slack event_time epoch seconds; Shopify created_at RFC 3339) → UTC ISO + epochSeconds. Store the epoch, render the ISO. (5) redact the body before it touches any log. Persist ONLY the redacted string; report per-kind counts as metrics. Final return: {accepted: <signature valid AND schema valid>, verify: {valid, scheme, reason}, schemaValid: bool, schemaErrors: [], fingerprint: <sha256 hex>, eventTimeUtc, eventEpochSeconds, redactedBody, redactionCounts, oneLineSummary: 'webhook accepted: signature ok, envelope ok, deduped by fingerprint, 1 email redacted, safe to process'}. All five tools are pure-CPU and PoW-eligible - the whole gate runs on the free tier. Budget ≤ $0.01 even paid.