Webhook Sentinel (Stripe) v1.1.0

Webhook Sentinel (Stripe) is the webhook security layer Bubble was always missing. It verifies the authenticity of incoming Stripe webhook requests using cryptographic signature verification, ensuring that only legitimate, unmodified events from Stripe are processed by your workflows.

No data collected This plugin does not collect user data.

Key features 6

Signature Verification, the Stripe Way

HMAC-SHA256 over the exact bytes Stripe signed, compared in constant time. Every v1 signature in the header is checked, not just the first, so legitimate events keep passing while you rotate the signing secret.

IP Allowlist

Stripe asks for two protections, not one: the signature proves the event was not written by a third party, and the allowlist refuses anything that did not come from Stripe servers. Comes pre-filled and off by default.

Replay and Environment Checks

A timestamp window stops captured payloads from being reused, defaulting to the same five minutes Stripe libraries use. The environment check reads live or test from inside the signed payload, and follows your app instead of being switched by hand.

Structured Rejection Reasons

Eight failure causes, each one as a text code for your logs and as one yes/no for your workflow conditions. Exactly one boolean is true whenever verification fails, so a condition never has to compare text.

The Verified Event, Unpacked

Event id, type, environment, creation time, age and the inner object id come back ready to use, so idempotency and reconciliation cost no extra parsing. Audit logging costs nothing at all: a log text field returns every value as one block, one per line, ready to store. They stay empty on rejection, because an unverified body is a body chosen by whoever sent it.

Server-Side, No Side Effects

Everything runs server-side, so your signing secret never reaches the client. The action stores nothing, creates nothing and changes nothing: what happens after verification is entirely up to your workflow.

In detail

Stripe - Verify Webhook Signature

  • Cryptographic HMAC-SHA256 signature verification, the same method used by Stripe's official SDKs
  • Constant time comparison, so a rejected signature never reveals how close it came
  • Timestamp freshness check against replay attacks, with a configurable tolerance window and a safe minimum floor
  • Accepts every signature present in the Stripe-Signature header, so legitimate events keep passing while you rotate the signing secret in Stripe
  • Reads the body both as it arrives and in the exact form Stripe signed, so a real event is never rejected over formatting
  • Optional environment check that follows your app: a dynamic yes/no field instead of a fixed option, so the check moves with your deploys instead of being switched by hand
  • Optional IP allowlist, because Stripe asks for both protections: the signature proves the event was not written by a third party, the allowlist refuses what did not come from Stripe servers
  • Both optional checks are off by default, so updating the plugin never changes the result of a workflow that already works

Everything the verified event hands you

  • Event id, the key to store so a redelivered event is never processed twice
  • Event type, to route the workflow without parsing the body a second time
  • Inner object id and inner object type, the pair you look up in the Stripe API to confirm the current state at the source instead of trusting the payload
  • Event created and event age in seconds, for your audit trail
  • Event environment, live or test
  • A ready-made log line with every returned value, one per line, to store as an audit record or paste into a support ticket without rebuilding it in a workflow
  • All of these are returned only when the signature is valid: an unverified body is never handed back as data

Built for conditions, not for string matching

  • Rejection reason as a stable text code for logs and audit records, one for each of the eight failure causes
  • One yes/no per cause for workflow conditions, because a typo in a text comparison never matches and never warns
  • Exactly one of them is true whenever verification fails, with no exception to remember

Verify only

  • Runs on the server, before any business logic in your workflow
  • The action never stores data, creates records, or produces side effects

How to use 6

  1. 01
    Create the Backend Workflow endpoint

    In Backend Workflows, create an API event, tick "This workflow can be run without authentication" and "Include headers in the detected data", and set the request data type to raw. Stripe needs the body exactly as it was sent.

  2. 02
    Register the endpoint in Stripe

    In the Stripe Dashboard, under Developers and then Webhooks, add your Bubble endpoint URL and copy the signing secret. It starts with whsec_ and is different for test mode and live mode.

  3. 03
    Put the action first

    Add Stripe - Verify Webhook Signature as the first step. Fill the secret, the Stripe-Signature header, the raw body and the tolerance. Optionally turn on the livemode check to refuse events from the other environment.

  4. 04
    Branch on the result

    Terminate the workflow when is valid is false. Use the yes/no per cause in the conditions and write the rejection reason text into your log. Everything after this step should only run for legitimate events.

  5. 05
    Store the event id before acting

    Stripe redelivers events. Save the returned event id in a field with a unique constraint and stop when it already exists, so the same event is never processed twice.

  6. 06
    Confirm at the source

    A webhook is a notice, not a verdict. Use the inner object id to fetch the current object from the Stripe API and act on what comes back, because events can arrive out of order.

HMAC SIGNATURE CHECK (REQUIRED) 4

Use this action only in Backend Workflow events. The inputs must be passed exactly as they arrive: any parsing or reformatting of the raw body breaks the signature even when the request is legitimate.

Stripe signing secret

text

The webhook signing secret associated with your Stripe Webhook. Find this in the Stripe Dashboard under Developers → Webhooks → Your endpoint → Signing secret. Always starts with "whsec_". Never expose this value in client-side code or logs.

defaultno default

Stripe signature

text

The Stripe-Signature header sent by Stripe with every webhook request. In Bubble, use Event Request Data's headers stripe-signature.

defaultno default

Raw body

text

The raw body string of the incoming request. In Bubble, use Event Request Data's raw body text. Must be the original unmodified body, any parsing or reformatting will cause the signature verification to fail, even if the request is legitimate.

defaultno default

Tolerance seconds

number

Tolerance in seconds for validating the event timestamp. Events older than this value are rejected to prevent replay attacks. Defaults to 300 (5 minutes), which is the same tolerance Stripe's own libraries use. The minimum is 30 seconds and lower values are raised to it on purpose: Stripe documents a tolerance of 0 as a common mistake, because it turns the recency check off entirely. Increase only if your server has known clock sync issues, and prefer fixing the clock with NTP, which is what Stripe recommends.

default300

The 30 second floor is not arbitrary: Stripe lists a tolerance of 0 as a common mistake, because it turns the recency check off entirely. If events are being rejected as expired, fix the server clock with NTP rather than raising this value, which is also what Stripe recommends.

LIVEMODE CHECK (OPTIONAL) 2

Enable livemode check

yes/no

Turns on the livemode check. Off by default, and off means the environment is not verified at all. When it is on, [Accept events from the live version] must have a value, otherwise every event is rejected with rejection_reason "livemode_check_misconfigured". The environment is read from inside the signed payload, so it cannot be forged. This does not replace the separation the signing secrets already provide, since the test and live endpoints have different secrets: it is the net for when the same secret ends up configured in both environments.

defaultno

optional

Accept events from the live version?

yes/no

The plugin reads the environment from inside the signed event and compares it with this field. Yes accepts only live events, no accepts only test events, and a test version includes any development branch. Point it at Bubble's own Isn't live version with is "no" to build an environment check that compares the two modes, the event's and the app's, so the check follows your deploys. Pointing it at Isn't live version alone inverts the answer and rejects every real event. Only read when [Enable livemode check] is on.

defaultno default

optional

IP ALLOWLIST CHECK (OPTIONAL) 3

Enable IP check

yes/no

Turns on the IP allowlist check. Off by default, and off means this action behaves exactly as it did before: only the signature, the timestamp and the environment are verified. When it is on, both [IP allowlist] and [Request IP] must be filled, otherwise every event is rejected with rejection_reason "ip_check_misconfigured". That is deliberate: a security check you turned on and left unconfigured should fail loudly, not pretend to protect you.

defaultno

optional

IP allowlist

text

The IP addresses allowed to deliver webhooks, one per line or separated by commas. Comes pre-filled with the list Stripe published when this plugin version was released. That list is a snapshot, not a live feed: check https://stripe.com/files/ips/ips_webhooks.json before relying on it. Stripe changes these addresses over time, and an address they add that is missing here becomes a legitimate event rejected with "ip_not_allowed". Exact addresses only, CIDR ranges are not supported. Only read when [Enable IP check] is on.

default3.18.12.63 3.69.109.8 3.120.168.93 3.130.192.231 13.235.14.237 13.235.122.149 18.211.135.69 35.154.171.200 35.157.207.129 52.15.183.38 54.88.130.119 54.88.130.237 54.187.174.169 54.187.205.235 54.187.216.72

optional

The field comes pre-filled with the list Stripe published when this plugin version was released. That is a snapshot, not a live feed: confirm the current list at https://stripe.com/files/ips/ips_webhooks.json, because an address Stripe adds that is missing here turns a legitimate event into a rejection.

Request IP

text

The IP address the request came from. Use Request Data's header cf-connecting-ip, not x-forwarded-for. The difference matters: x-forwarded-for is written by whoever makes the request and can be forged, so an attacker can simply claim to be a Stripe address, while cf-connecting-ip is written by Cloudflare, which sits in front of Bubble, and is overwritten on every request. If a chained value is passed anyway, only the last address in the chain is checked, which is the one added by the infrastructure and not by the caller. Only read when [Enable IP check] is on.

defaultno default

optional

Use Request Data's header cf-connecting-ip, not x-forwarded-for. The second one is written by whoever makes the request, so an attacker can simply claim to be a Stripe address and pass this check; the first is written by Cloudflare, which sits in front of Bubble, and is overwritten on every request. If a chained value arrives anyway, only the last address in the chain is checked, which is the one the infrastructure added and not the one the caller invented. Stripe asks for this check and signature verification, not one or the other: they cover different attacks.

Returned values 18

What the action hands back to the workflow that called it.

The event fields are filled in only when the signature is valid. On a rejection they come back empty, on purpose: an unverified body is not a source of data.

is valid

yes/no

Yes when the signature matches, the event is within the tolerance window, and the environment check passed. No otherwise. Treat it as the gate: terminate the workflow when it is no, and do not process the event any further.

rejection reason

text

The cause of the rejection, as a stable text code. Empty when [is valid] is yes. This is the value to write into logs and audit records; for branching the workflow, use the yes/no below instead, because a typo in a text comparison never matches and never warns.

  • missing_inputsone or more required fields are empty. The action cannot proceed without a secret, a signature and a raw body.
  • malformed_headerthe Stripe-Signature header is missing or has an unexpected format. The timestamp or the signature value could not be extracted.
  • signature_mismatchthe computed signature does not match any signature received. The request did not come from Stripe, or the payload was modified in transit.
  • timestamp_expiredthe event is older than the configured tolerance. The request may be a replay attack.
  • livemode_check_misconfiguredthe livemode check is on but Accept events from the live version has no value, so there was nothing to compare the event against. The action refuses instead of guessing one of the two answers, because guessing would silently reject every event from the other environment.
  • livemode_mismatchthe event is authentic, but it comes from the environment you chose to refuse in Accept events from the live version.
  • ip_check_misconfiguredthe IP check is on but the allowlist or the request IP is empty, so there was nothing to check against. The action refuses instead of letting the event through, because a check you turned on and left unconfigured should fail loudly rather than quietly protect nothing.
  • ip_not_allowedthe signature, the age and the environment all checked out, but the request came from an address that is not in your allowlist. The most common cause is not an attack: it is Stripe having added an address that your list does not have yet.

is missing inputs

yes/no

Yes when the secret, the signature or the raw body arrived empty. Nothing was verified: this is a configuration problem in your own workflow, not a suspicious request, and it deserves an alert to the developer rather than a silent log line.

is malformed header

yes/no

Yes when the Stripe-Signature header is missing or has an unexpected format, so the timestamp or the signature could not be extracted. Usually means the wrong header was mapped into the field.

is signature mismatch

yes/no

Yes when no signature in the header matches the one computed from your secret and the body. Either the request did not come from Stripe, or the body was altered on the way, or the secret belongs to another endpoint.

is timestamp expired

yes/no

Yes when the signature is authentic but the event is older than the tolerance window. The request may be a replayed copy of a legitimate event, or your server clock may be out of sync.

is livemode check misconfigured

yes/no

True when the livemode check is on but Accept events from the live version is empty. Like the IP one, this is not about the event: it means your own setup is incomplete, so treat it as an alert rather than something to log and move on from.

is livemode mismatch

yes/no

True when the event is authentic but comes from the environment you chose to refuse in Accept events from the live version. The environment is read from inside the signed payload, so it cannot be forged.

is ip check misconfigured

yes/no

True when the IP check is on but IP allowlist or Request IP is empty, so there was nothing to check against. This one is not about the event: it means your own setup is incomplete and the check is protecting nothing, so treat it as an alert rather than something to log and move on from.

is ip not allowed

yes/no

True when the event is authentic but came from an address that is not in IP allowlist. It is the only cause that can be true while the signature itself is valid, so it means the event is genuine and was refused on origin alone.

event environment

text

The environment the event was created in, live or test. It is read from inside the signed payload, so it cannot be forged. Useful in the audit line, where live or test reads better than a yes/no.

  • livethe event was created in live mode.
  • testthe event was created in test mode.

event id

text

The Stripe event id, such as evt_1UMBru... This is the key to store so a redelivered event is never processed twice: Stripe retries delivery, and the same event arrives with the same id.

event type

text

The event type, such as checkout.session.completed. Use it to route the workflow without parsing the body a second time.

event created unix

number

When Stripe created the event, as a Unix timestamp in seconds. It is the envelope's own timestamp, which can be a few seconds later than the creation time of the object inside it.

event age seconds

number

How many seconds passed between the signature timestamp and the moment the action ran. The same number the tolerance check uses, exposed so your audit trail can record how late an accepted event was.

inner object id

text

The id of the object carried by the event, such as cs_test_... Look it up in the Stripe API to confirm the current state at the source, instead of trusting what the payload says, because events can arrive out of order.

inner object type

text

The type of the object carried by the event, such as checkout.session. It tells you which Stripe endpoint to call with [inner object id].

log text

text

Every returned value above, as one text block, one per line, in the form name: value. Booleans come as yes and no, and empty values keep their line so two executions stay comparable. Built for storing an audit line or pasting into a support ticket without rebuilding the list by hand in a workflow. It carries only returned values: the signing secret, the signature and the raw body are never in it, so this text is safe to store.

FAQ 11

What is Webhook Sentinel (Stripe)?

It is a server-side action that proves an incoming webhook really came from Stripe. It recomputes the HMAC-SHA256 signature over the exact payload Stripe signed, compares it with the Stripe-Signature header, checks how old the event is, and returns the verdict along with the data of the verified event. It does nothing else: what happens after the verdict is your workflow's decision.

Why do I need this plugin? Can't Bubble verify Stripe webhook signatures natively?

No. Bubble does not support native webhook signature verification. Without it, anyone who knows your endpoint URL can send a fake event and trigger your workflow. Signature verification is the only layer that cryptographically proves a request came from Stripe and that the payload was not modified in transit. Stripe recommends it as the primary security layer for webhook endpoints, and while other layers like event idempotency or environment isolation are valuable additions, none of them replace it.

Does it protect against replay attacks?

Yes. The action checks the timestamp embedded in the Stripe-Signature header and rejects events older than the configured tolerance window. This prevents a captured valid payload from being reused after the window expires. The tolerance is configurable with a minimum floor of 30 seconds and a recommended default of 300 seconds (5 minutes) by Stripe.

How does the IP allowlist check work, and do I need it?

Stripe asks for it alongside signature verification, because the two cover different attacks: the signature proves the event was not written or modified by a third party, and the allowlist refuses anything that did not come from Stripe servers in the first place. The check is off by default. When you turn it on, pass Request Data's header cf-connecting-ip, not x-forwarded-for: the second one is written by whoever makes the request and can be forged. The allowlist comes pre-filled with the list Stripe published when this plugin version was released, and it is a snapshot, not a live feed, so confirm the current list at https://stripe.com/files/ips/ips_webhooks.json before relying on it.

Where should I place the action in my workflow?

As the first step. If the signature is invalid or the timestamp is expired, terminate the workflow immediately, do not process the event further. Optionally, log the rejection reason before terminating to keep a record of invalid requests. Everything after the action should only run for legitimate events.

What does rejection reason return?

A stable text code identifying why the request was rejected. There are eight: missing_inputs, malformed_header, signature_mismatch, timestamp_expired, livemode_check_misconfigured, livemode_mismatch, ip_check_misconfigured and ip_not_allowed. It is the value to write into logs and audit records. For branching the workflow, use the yes/no returned for each cause instead, because a typo in a text comparison never matches and never warns. Exactly one of those booleans is true whenever verification fails.

What happens during a signing secret rotation in Stripe?

Nothing breaks. While a rotation is in progress Stripe sends more than one signature in the same header, one per active secret. The action checks all of them, so events signed with either secret keep being accepted until you finish the rotation.

Why are the event fields empty when the signature is invalid?

Because an unverified body is a body chosen by whoever sent it. Returning its event id would hand your workflow the exact value it uses to deduplicate and to write into the audit log, supplied by the sender. Event id, type, environment, creation time and the inner object id only come back when the signature checks out.

Can I add other security layers on top of this plugin?

Two of them are now inside it. Stripe asks for signature verification and IP allowlisting, and both are available here, with the allowlist optional and off by default. The environment check, accepting only live or only test events, is also built in. What remains for your workflow is event idempotency, blocking an event id you already processed, minimum data validation, and a reconciliation job to catch events that never arrived. None of these replace signature verification; they build on top of it.

Does the action store any data or create any records?

No. The action only verifies the signature and returns the result. It does not store any data, create any records, or produce any side effects. Any logging or record creation is entirely up to your workflow.

How often is the plugin updated?

The frequency with which we update our plugins depends on several factors: 1 - If there are bugs (known or reported by users) that need to be fixed. 2 - If we receive requests for new features that we consider essential for users. 3 - If the plugin depends on a library and it has been updated with a stable version. 4 - If we decide to add more features to the plugin or make it easier to use. 5 - Whenever we determine that the code can be optimized for better performance. 6 - When plugin information, such as the logo, description, demo links, etc., needs to be updated. 7 - When Bubble makes changes to its terms or updates its API version.