Skip to content

Verified ingress

Knocker keeps durable receipt semantics in SQLite. Bindings adapt host-language request objects and call shared ingress primitives; curated provider verification lives in the SQLite/Rust layer.

Across the product, provider support has these tiers:

  • Curated built-in providers ship in Knocker’s shared provider set and have repo-owned conformance fixtures under providers/<name>/. Today: stripe, github, shopify, slack, postmark, resend, paddle, lemon-squeezy, standard-webhooks, clerk, twilio, sendgrid, linear, meta, discord, zendesk, intercom, hubspot, token-header, bearer-token, and basic-auth. The string provider="..." namespace is reserved for these names across bindings.
  • Binding-local provider extension points live in host-language code. Today this custom-provider object interface exists in the Python binding. Curated providers should still move into shared Rust/SQLite code.

String provider names (provider="...") are reserved for curated built-ins. Custom providers should not compete in that string namespace.

If you need a project-local hotfix before a provider fix lands in core, use your binding’s local extension point where available and give it a distinct name.

Curating a new provider goes through the Curated provider contribution guide.

These names work through the shared SQLite/Rust knocker_receive(...) path and are available to every binding that exposes receive(...).

ProviderVerification shapeNotes
stripeStripe-Signature timestamped HMAC-SHA256Supports overlapping secrets and provider_options={"tolerance_s": 300}. Extracts JSON id and type.
githubX-Hub-Signature-256: sha256=<hex> over the raw bodyUses X-GitHub-Delivery as the dedupe identity and X-GitHub-Event as the event type.
shopifyX-Shopify-Hmac-Sha256 base64 HMAC-SHA256Uses Shopify webhook/event headers for delivery id, provider event id, and event type.
slackX-Slack-Signature over v0:{timestamp}:{body}Supports timestamp tolerance and extracts event_id / nested event type where present.
postmarkHTTP Basic AuthExtracts MessageID and RecordType from JSON bodies.
resendSvix/Standard Webhooks signature headersExpects Svix-style whsec_... secrets and extracts svix-id, data.email_id, and type.
paddlePaddle-Signature timestamped HMAC-SHA256Extracts event_id and event_type from JSON bodies.
lemon-squeezyX-Signature hex HMAC-SHA256Extracts the event name plus payload identifier from JSON bodies.
standard-webhooksStandard Webhooks/Svix webhook-id, webhook-timestamp, webhook-signatureAlso accepts svix as an alias. Expects Svix-style whsec_... secrets.
clerkSvix/Standard Webhooks signature headersSame signature scheme as Standard Webhooks, with Clerk’s webhook headers.
twilioX-Twilio-Signature over configured public URL plus sorted form/query paramsRequires provider_options={"url": "https://example.com/webhooks/twilio"}. This is the classic Twilio request-validation shape.
sendgridTwilio SendGrid ECDSA Event Webhook signatureSecrets are PEM public keys. Supports timestamp tolerance.
linearLinear-Signature HMAC-SHA256Supports timestamp tolerance when the body contains webhookTimestamp.
metaX-Hub-Signature-256: sha256=<hex> over the raw bodyCovers Meta/Facebook-style webhook signatures.
discordEd25519 interaction signature headersSecrets are Discord public keys.
zendeskTimestamped HMAC-SHA256Verifies Zendesk’s timestamp plus raw-body signature shape.
intercomX-Hub-Signature: sha1=<hex> over the raw bodyExtracts id and topic / type from JSON bodies.
hubspotHubSpot v3 base64 HMAC-SHA256 over method, URL, body, and timestampRequires provider_options={"url": "https://example.com/webhooks/hubspot"}; method defaults to POST.
token-headerConstant-time comparison of a configured header valueDefaults to X-Knocker-Token; use provider_options={"header": "x-my-secret"} to choose another header.
bearer-tokenAuthorization: Bearer <secret>Useful for Zapier, Make, n8n, and other middleman tools that can set an Authorization header.
basic-authAuthorization: Basic <base64(secret)>Useful when a webhook sender can set Basic Auth but not provider-specific HMAC signatures.

Every curated provider has repo-owned fixtures under providers/<name>/. Those fixtures are exercised by Rust core tests and binding mirror tests so the documented behavior stays tied to executable examples.

add_endpoint(
name="stripe",
path="/webhooks/stripe",
provider="stripe",
secrets=["whsec_old", "whsec_new"],
provider_options={"tolerance_s": 300},
)

The Stripe provider:

  • verifies the Stripe-Signature header
  • accepts overlapping active secrets for rotation
  • enforces the configured timestamp tolerance (default 300 seconds)
  • extracts the upstream event id from the JSON body
  • extracts the event type from the JSON body

provider_options is schema-checked. Unknown keys raise ValueError at endpoint registration time so typos do not silently no-op.

add_endpoint(
name="github",
path="/webhooks/github",
provider="github",
secrets=["github-webhook-secret"],
)

The GitHub provider:

  • verifies X-Hub-Signature-256 as sha256=<hex hmac> over the raw body
  • extracts X-GitHub-Delivery and uses it as Knocker’s dedupe identity
  • extracts X-GitHub-Event as the event type

GitHub’s X-GitHub-Delivery is stable across operator-initiated redeliveries from the GitHub dashboard, so Knocker treats a redelivery of the same delivery id as a duplicate. To force reprocessing, use webhooks.replay(...) on the stored event instead.

Zapier, Make, n8n, and similar tools often make it easy to send a secret header or Authorization value, but not to compute a provider-specific HMAC over the raw body. Knocker ships three simple curated providers for that shape:

  • token-header: compares a configurable header, defaulting to X-Knocker-Token
  • bearer-token: compares Authorization: Bearer <secret>
  • basic-auth: compares Authorization: Basic <base64 secret>
add_endpoint(
name="zapier",
path="/webhooks/zapier",
provider="token-header",
secrets=["shared-secret"],
)

Use provider_options={"header": "x-my-secret"} with token-header when your automation tool already has a fixed header name.

The Python binding also exposes a custom-provider object interface for project-local providers. Implement the small Provider interface and pass your instance directly to add_endpoint(...). Curated string names like "stripe", "github", "shopify", "slack", "postmark", "resend", "paddle", "lemon-squeezy", "standard-webhooks", "clerk", "twilio", "sendgrid", "linear", "meta", "discord", "zendesk", "intercom", "hubspot", "token-header", "bearer-token", and "basic-auth" are reserved for built-ins; non-curated providers should pass an instance.

import hmac
import knocker
class AcmeProvider(knocker.Provider):
name = "acme"
version = "1.0.0"
option_keys = frozenset({"clock_skew_s"})
requires_secrets = True
def verify(self, request, *, secrets, options):
token = request.header("x-acme-token")
delivery_id = request.header("x-acme-delivery")
event_type = request.header("x-acme-event")
for secret in secrets:
if hmac.compare_digest(token or "", secret.decode("utf-8")):
return knocker.ProviderResult.accept(
provider_delivery_id=delivery_id,
event_type=event_type,
)
return knocker.ProviderResult.reject(
"acme token mismatch",
provider_delivery_id=delivery_id,
event_type=event_type,
)
webhooks = knocker.open("knocker.db")
webhooks.add_endpoint(
name="acme",
path="/webhooks/acme",
provider=AcmeProvider(),
secrets=["acme-secret"],
)

Python community providers ship as ordinary packages that expose a Provider value. Apps register them at add_endpoint(...) time:

import knocker
import acme_webhooks # third-party community package
webhooks.add_endpoint(
name="acme",
path="/webhooks/acme",
provider=acme_webhooks.provider(),
secrets=["acme-secret"],
)

Providers passed as instances are scoped to that endpoint. They do not appear in provider_versions(), which only reflects the curated built-ins. There is no global string-lookup registry for non-curated providers in Knocker; the instance path is the only path.

Provider.verify(...) returns a ProviderResult that combines the verification outcome and the metadata the provider could extract. Extraction should happen before the signature check so an invalid receipt still produces a useful orphan delivery row with provider delivery id, event id, and event type populated where available.

ProviderRequest exposes case-insensitive request.header(name) and request.json(). request.json() raises ValueError on non-JSON bodies and caches its parsed result; guard with try/except for endpoints that may receive non-JSON payloads.

If your Provider.verify(...) raises an unexpected exception, Knocker turns that into a verification failure with a useful signature_error and stores an orphan delivery rather than crashing the caller.

provider_versions()

Bindings may expose provider-version inspection for debugging and release notes. Provider versions are not part of any Knocker compatibility contract.

Generic HMAC has too many per-provider knobs (header name, prefix, secret format) to ship as a curated provider without diluting the support promise. It stays available as the explicit verification={...} config path:

add_endpoint(
name="acme",
path="/webhooks/acme",
verification={
kind="hmac-sha256",
header="x-acme-signature",
prefix="sha256=",
secrets=["old-secret", "new-secret"],
},
delivery_key=<read x-acme-delivery-id>,
event_key=<read x-acme-event-id>,
)

The legacy verification config:

  • kind="stripe" (kept for compatibility; the Stripe provider runs underneath)
  • kind="hmac-sha256" (also accepts generic-hmac-sha256 and hmac)
  • one secret or many secrets
  • optional prefix for generic HMAC
  • optional tolerance_s for Stripe

Generic HMAC signs only the request body. Use a provider-specific verifier or custom verification path when you need timestamp-based replay protection.

Every inbound receipt becomes a Delivery, including invalid signatures.

  • valid receipt: stores a Delivery, creates or correlates an Event, and may enqueue work
  • invalid receipt: stores a Delivery only, with event_id=None

Invalid orphan deliveries still surface provider-extracted metadata where the provider was able to read it before the signature check failed, so you can inspect bad receipts without losing them.

Knocker separates delivery-level identity from event-level identity:

  • delivery_key identifies one upstream HTTP receipt
  • event_key identifies one upstream business event

Built-in providers fill these in automatically. For app-local providers, populate them in your ProviderResult. You can also pass delivery_key=... or event_key=... to add_endpoint to override what the provider extracted; explicit receive(...) arguments win over both.

Most users should call receive(...).

ingest(...) is the lower-level contract entry point when you already know the verification result and extracted metadata and want to drive the durable rows directly. It is trusted ingress: passing signature_valid=True means Knocker will store and enqueue as valid without running a verifier.