Glossary

Stripe Webhook

A Stripe webhook is an HTTPS endpoint you register with Stripe so that Stripe can POST an Event object to it whenever something changes in your account, which is how any accounting integration learns that a charge, refund, or payout happened without polling for it.

Also called: webhook endpoint, event destination, Stripe event, webhook event

Definition

Stripe does not know your bookkeeping system exists. What it knows is that something changed: a card was charged, an invoice was paid, a payout left for the bank. A webhook is the arrangement by which Stripe tells you.

Mechanically it is unremarkable. You register an HTTPS URL. Stripe sends it an HTTP POST with a JSON body whenever a subscribed event occurs. Your server reads the body and does something. Stripe describes it as creating an endpoint so that "Stripe pushes real-time data to it when events happen in your Stripe account".

The reason the term shows up constantly in Stripe-to-QuickBooks conversations is that webhooks are the only reason your books can be current. Without them, an integration has to poll, which means it either asks too often and burns rate limit or asks too rarely and shows you yesterday. With them, the sale that happened ninety seconds ago can already be in QuickBooks.

What trips people up is not the mechanism. It is the guarantees. Webhooks are not a queue you can trust to arrive once, in order, exactly when the thing happened. They arrive eventually, possibly twice, possibly out of sequence, and the difference between an integration that handles that and one that does not is most of the difference between books you can close and books you cannot.

Key points

  • +A webhook endpoint is a publicly accessible HTTPS URL you register with Stripe. You can register up to 16 of them.
  • +Stripe delivers an Event object as a JSON payload over HTTP POST. The endpoint should return a 2xx status quickly, before any slow work.
  • +The Event object carries id, type, created, livemode, api_version, data, request, pending_webhooks, and for Connect, account.
  • +data.object is a snapshot of the object as it looked when the event fired. Stripe: "The contents of data never change."
  • +Stripe signs every event. The Stripe-Signature header holds a timestamp and an HMAC-SHA256 signature computed over the raw request body with a signing secret that begins with whsec_.
  • +Signature verification needs the unmodified raw body. Any framework that reparses or rewrites it breaks verification.
  • +Live-mode delivery is retried with exponential backoff for up to three days. Sandbox events are retried three times over a few hours.
  • +Ordering is not guaranteed. Stripe: "Stripe doesn’t guarantee the delivery of events in the order that they’re generated."
  • +Duplicates happen. Stripe advises logging event IDs and skipping ones you have already processed, and warns against using created to order or deduplicate.
  • +A 3xx redirect counts as a delivery failure, not a success. So does a timeout.
  • +Events stay retrievable through the API for 30 days. Manual resend is 15 days from the Dashboard and 30 days from the CLI.

The Event object is a photograph, not a live feed

The single most useful thing to understand about a Stripe event is that it is frozen at the moment it was created.

The payload contains a data.object, which is the full API object as it existed when the event fired. Stripe is explicit that this never changes afterwards: the api_version field records the version used to render data when the event was created, and "The contents of data never change, so this value remains static regardless of the API version currently in use." Update your account to a newer API version and old events keep their old shape. Retrieve a two-week-old event with today’s API version and you still get the shape it was born with.

That is a feature when you want to know what happened, and a trap when you want to know what is true now. If a charge was created and then refunded four minutes later, the charge.succeeded event still describes an unrefunded charge, because that is what was true when it fired. Anything acting on stale event data rather than re-reading the object can post a record that was already obsolete when it arrived.

The other fields earn their place too. type names the event, like charge.succeeded or invoice.paid. livemode separates real activity from test activity, which matters more than it sounds because a test charge looks identical to a real one in the interface if you are not watching the mode toggle. request identifies the API call that caused the event, if there was one. pending_webhooks counts deliveries that have not yet succeeded. For a Connect platform, account names the connected account the event came from, which is the only thing distinguishing one seller’s activity from another’s.

Delivery is at-least-once, unordered, and retried for three days

Three properties of Stripe’s delivery model decide how an integration has to be built, and all three surprise people.

First, order is not guaranteed. Stripe states it directly: "Stripe doesn’t guarantee the delivery of events in the order that they’re generated." Creating a subscription can produce customer.subscription.created, invoice.created, invoice.paid, and charge.created, and they may arrive in any sequence. Stripe adds a second warning that catches people trying to sort their way out of it: snapshot events record created in seconds, so distinct events can share a timestamp, and "Don’t use created to determine event order or whether you’ve already processed an event."

Second, delivery is at least once, not exactly once. Stripe says endpoints "might occasionally receive the same event more than once" and recommends logging event IDs and skipping ones already seen. There is a subtler case too: sometimes two separate Event objects are generated for the same underlying change, and telling those apart needs the ID of the object in data.object together with the event type, because the event IDs differ.

Third, failure is patient. In live mode Stripe retries with exponential backoff for up to three days. In a sandbox it retries three times over a few hours. A retried delivery gets a fresh signature and timestamp, so a replay-protection window measured against the original timestamp will not reject it.

For bookkeeping, the practical consequence of all three is that duplicate protection cannot be an afterthought. An event arriving twice must not produce two sales receipts, and an event arriving late must not produce a record dated today for a sale that happened on Tuesday.

Signature verification, and the raw-body rule that breaks it

An unverified webhook endpoint is an open instruction channel. Anyone who learns the URL can POST a plausible JSON body and cause whatever the handler causes. Stripe puts the risk plainly: without verification "an attacker could send fake webhook events to your endpoint to trigger actions like fulfilling orders, granting account access, or modifying records."

The defence is the Stripe-Signature header. It carries a timestamp with a t= prefix and one or more signatures with a scheme prefix, of which v1 is the only valid live scheme. Stripe computes each signature as an HMAC using SHA-256, keyed on the endpoint’s signing secret, over a message built by concatenating the timestamp, a period, and the exact JSON request body. Verifying means recomputing that and comparing in constant time.

The timestamp is not decoration. Because it is inside the signed payload, an attacker cannot alter it without invalidating the signature, which is what makes it useful against replay attacks. Stripe’s libraries default to a five-minute tolerance and warn against setting the tolerance to zero, since that disables the recency check entirely rather than tightening it.

The failure mode almost everyone hits once is the raw body. Signature verification is computed over the bytes Stripe sent, so any middleware that parses JSON and re-serialises it, or that rewrites whitespace, produces a different message and a failed check. Stripe repeats the warning twice on its own page: "Stripe requires the raw body of the request to perform signature verification." A JSON body-parser mounted globally in a web framework is the usual culprit, and the symptom is every event failing verification while the payload looks perfectly correct in the logs.

Stripe recommends pairing signature verification with IP allowlisting, since webhook events come from a published set of addresses. Two independent checks, because either one alone can be misconfigured silently.

Why your endpoint must answer before it works

The instruction Stripe gives about response timing reads like a performance tip and is actually an architectural constraint.

Stripe asks endpoints to "quickly return a successful status code (2xx) before any complex logic that might cause a timeout", and gives an accounting example on its own initiative: you must return a 200 before updating a customer’s invoice as paid in your accounting system. If the handler waits, the delivery is recorded as a timeout and Stripe retries, which means the slow work now runs twice.

The shape that follows is a handler that verifies the signature, writes the event to a queue, and returns. Everything real happens afterwards, asynchronously. Stripe recommends exactly this, and points at the reason volume-driven businesses need it: a spike, such as the start of a month when every subscription renews, can overwhelm an endpoint that processes synchronously.

A few other delivery failures are worth recognising because they look like nothing is wrong. A 3xx redirect is treated as a failure rather than followed, so moving an endpoint and leaving a redirect behind silently stops delivery. A 4xx from access restrictions, or a 404, fails the same way. TLS below version 1.2 fails the connection outright. All of these appear in the Event deliveries tab of the endpoint in Workbench, with the HTTP status of each attempt, which is the place to look rather than the events list itself.

That distinction matters more than it sounds. The events list in your account records things that happened to your data, and those Event objects exist whether or not anyone is subscribed. Delivery is tracked separately, per endpoint. An event sitting in the list with no delivery row against a given endpoint was never sent there, and no amount of staring at the events list will show you that.

How Acodei uses Stripe webhooks

Acodei is an event-driven pipeline, and its documentation describes the whole product in one line: Stripe emits webhooks, Acodei turns each relevant event into a Transaction record, and a queue of specialized jobs writes the corresponding QuickBooks records.

The receiving end is a separate service from the worker that writes to QuickBooks. It serves three webhook routes: one for Stripe v1 Connect events, one for v2 money-management and Financial Account events, and one for Acodei’s own billing. Signature verification runs against the Stripe-Signature header, checked over the raw request body with a per-endpoint signing secret, before anything is dispatched.

Two filters then run at the edge, before the event reaches a queue, and both apply only to the v1 Connect route. The v2 route is deliberately unfiltered, because a paused account still receives Financial Account events. The first filter drops events for connected accounts that are paused, never linked a QuickBooks company, or carry a connection record no longer pointing at a live QuickBooks company. The second drops ten per-charge event types for accounts on daily-summary sync, because those types already do nothing in the worker for such an account: their figures are re-fetched from the Stripe API when a payout or reporting event arrives. Payout events, reporting events, top-up successes, customer updates, and account lifecycle events are always kept.

Both filters fail open. On a cache miss, timeout, or error the event dispatches anyway, so the failure mode is a redundant event getting processed rather than a real one being dropped. Account lifecycle events bypass the filters entirely, since those are how the system learns about an account in the first place.

What survives becomes a Transaction row carrying the event type, the extracted amounts, the currency, and the balance-transaction detail flattened into columns. Routing is by event type: a successful charge becomes a Sales Receipt, or a Payment on accounts configured that way; a refunded charge becomes a Refund Receipt; an invoice created in Stripe becomes a QuickBooks Invoice; a paid payout becomes a Transfer or a Deposit depending on the holding-account setup; a successful top-up becomes a Transfer from the bank into the holding account.

One guard is worth naming because it is the direct answer to at-least-once delivery. Each Transaction carries a flag set while a job is mid-run, so a second worker picking up the same work returns early instead of writing the record twice. The Data Feed in the dashboard is the user-facing ledger of every synced and errored transaction, and it is the fastest way to tell an event that was filtered from one that arrived and failed: a filtered event never produces a row there at all.

Want to see this on your own Stripe data?

Start a free trial

Frequently asked questions

What is a Stripe webhook?

It is an HTTPS endpoint you register with Stripe so Stripe can send you events. When something changes in your account, Stripe makes an HTTP POST to that URL with a JSON Event object describing what happened. It is how an integration learns about a charge, refund, or payout without repeatedly asking.

What is the difference between an event and a webhook?

An event is the record of something that happened. A webhook is the delivery mechanism. Stripe creates an Event object whenever the state of an API resource changes, and those objects exist whether or not anybody is listening. The webhook endpoint is what you register so that Stripe pushes those events somewhere. This is why an event can appear in your Stripe events list having never been delivered anywhere.

Does Stripe deliver webhook events in order?

No. Stripe states that it does not guarantee delivery of events in the order they were generated, and warns against using the created timestamp to determine order, because snapshot events record created in seconds and distinct events can share a timestamp. An integration has to tolerate arriving out of sequence rather than assuming a sequence.

Can the same Stripe event arrive twice?

Yes. Stripe says endpoints might occasionally receive the same event more than once, and recommends logging the event IDs you have processed and skipping repeats. Separately, in some cases two distinct Event objects are generated for the same underlying change, and identifying those needs the ID of the object inside data.object together with the event type, since the event IDs will differ.

How long does Stripe retry a failed webhook?

In live mode Stripe retries with exponential backoff for up to three days. Events created in a sandbox are retried three times over the course of a few hours. You can also resend manually, which works for up to 15 days after the event from the Dashboard and up to 30 days using the Stripe CLI. A manual resend does not stop the automatic retries.

Why is my Stripe webhook signature verification failing?

The most common cause is a modified request body. Stripe computes the signature over the exact raw bytes it sent, so any framework or middleware that parses the JSON and re-serialises it produces a different message and a failed check. Stripe states the requirement directly: it requires the raw body of the request to perform signature verification. Other causes are using the wrong signing secret, since each endpoint has its own and test and live differ, or a server clock far enough out of step to fall outside the default five-minute tolerance.

Do I need to set up Stripe webhooks myself to sync to QuickBooks with Acodei?

No. Acodei receives your events as a Stripe Connect platform, so the endpoint that receives them belongs to Acodei rather than sitting in your Stripe account. That is also why you will not find an Acodei endpoint in your own webhook list or be able to open its delivery tab. If you are checking whether something synced, the Data Feed in the Acodei dashboard is the record to read, not your Stripe events list.

Why does my Stripe event count not match my QuickBooks record count?

It is not supposed to match. Some events are filtered before processing on purpose, some Stripe activity is not a sale at all, and on daily-summary accounts one payout absorbs many charges into a single record. A difference in counts is the normal state of a working sync rather than evidence of a problem.

What customers say about running Stripe through Acodei

Stripe Verified Partner BadgeQuickBooks Intuit Badge
If you're testing out all the different Stripe/QuickBooks integration apps right now, let me save you some time. This one is the best one by far.
RyanOwner at Indie Music Academy
Works well and is really helpful for massive transactions. The support is really fast and helpful. 100% recommended.
AndresCo-founder and CEO at Kanguro Collections and Reinsurance

Related reading

More glossary terms

See the full glossary

Ready to try Acodei?

Connect Stripe to QuickBooks Online in minutes and let the fees, refunds, and payouts land where your accountant expects them.