# Webhooks

URL: /workflows/webhooks · Id: `page:workflows/webhooks`

> Stripe, GitHub and any connected service push into a Startup; each delivery is checked, written once as an Event, and starts Workflows.

Stripe, GitHub and every other connected service push their news into a [Startup](/business/nouns/startup) as webhooks. Each delivery is checked, written once as an [Event](/business/nouns/event), and starts every [Workflow](/business/nouns/workflow) whose [Trigger](/business/nouns/trigger) names it.

## From delivery to Action
Every delivery takes the same four steps before any Workflow acts.

| Step | What happens | If it fails |
| --- | --- | --- |
| Verify | The sender's signature and timestamp are checked against the secret the platform holds | Refused with `401`; it never becomes an Event |
| Deduplicate | The sender's delivery id is looked up | Seen before: acknowledged, nothing written |
| Write | The payload becomes the record it describes and that record's Event; the raw body is kept beside it | Not acknowledged, so the sender retries |
| Acknowledge | `200` goes back only after the Event is written | None |

Then each Trigger that names the Event starts one [Action](/business/nouns/action). One Event never starts the same Workflow twice.

## Stripe
Every Startup runs its money on its own Stripe connected account, so there is no endpoint to register and no secret to store. Stripe's events reach the platform, which routes each one to its Startup. The left column uses Stripe's own names.

| Stripe sends | Arrives as | Then |
| --- | --- | --- |
| `payment_intent.succeeded` | [`Order.paid`](/business/events/Order.paid) | [Books](/workflows/processes/books) wakes on the first one |
| `invoice.paid` | [`Invoice.collected`](/business/events/Invoice.collected) | The [Subscription](/business/nouns/subscription) it bills turns `active` |
| `invoice.payment_failed` | [`Invoice.overdue`](/business/nouns/invoice#invoiceoverdue), with its `attempt` | [Billing](/workflows/processes/billing) wakes, and [Dunning](/workflows/processes/billing/dunning) tells the [Customer](/business/nouns/customer) |
| `charge.dispute.created` | [`Dispute.opened`](/business/nouns/dispute#disputeopened) | [Answer a chargeback](/workflows/processes/billing/answer-chargeback) starts |
| `payout.paid` | [`Transaction.settled`](/business/nouns/transaction#transactionsettled) | The [Transaction](/business/nouns/transaction) gets its `settledAt` |
| `outbound_payment.failed` | [`Transaction.failed`](/business/nouns/transaction#transactionfailed) | The [Bill](/business/nouns/bill) it was paying reopens |

Add a Workflow of your own on any of them. This one tells the founder about every chargeback the moment it opens:

```ts
on.Dispute.opened(async (dispute, $) =>
  $.Message.create({ medium: 'email', direction: 'outbound', to: 'founder', body: `Chargeback opened: ${dispute.reason}, ${dispute.held.amount} ${dispute.held.currency} held` }),
)
```

## GitHub
Connect GitHub once with [`Integration.connect`](/business/nouns/integration/connect) and pick the repositories. The platform installs its GitHub app there and holds the signing secret; GitHub signs each delivery with `X-Hub-Signature-256`.

| GitHub sends | Arrives as | Then |
| --- | --- | --- |
| `push` to a branch, when the repository is git-first | `Change.created` on that branch | [Change verdict](/code/verification/change-verdict) runs its checks |
| `issues`, opened | `Issue.created`, reported by a [Contact](/business/nouns/contact) matched by email | It joins triage with every other [Issue](/business/nouns/issue) |
| `issue_comment`, created | `Comment.created` on that Issue | The Issue's owning [Role](/business/nouns/role) sees it |
| An edit to a mirrored field | `Issue.updated` from that Contact | The Issue's [Permissions](/business/nouns/permission) decide whether it lands |

A db-first repository is a mirror: its pushes are refused, because the record lives in [db.sb](/code/platform/db-sb). [GitHub](/systems/integrations/github) covers both directions.

## Any other service
Connect it as an [Integration](/business/nouns/integration), and it gets one inbound URL: `https://api.sb/<namespace>/integrations/<integration>/inbound`. It signs each delivery with `webhook-id`, `webhook-timestamp` and `webhook-signature` headers, or with the scheme its Integration names.

A [Mapping](/business/nouns/mapping) in [`mappings/`](/code/file-conventions/mappings) turns its payload into a [Verb](/business/nouns/verb) on your own [Nouns](/business/nouns/noun). A form tool's submission becomes [`Lead.create`](/business/nouns/lead/create), so [Answer inbound](/workflows/processes/inbound-sales/answer-inbound) starts on `Lead.created`. A payload no Verb can take goes to a route in [`startup.ts`](/code/startup-ts#routes), with the reason recorded.

## Retries, order and testing
The ingress acknowledges only after the write, so a failure on the platform's side becomes a retry on the sender's. Stripe retries for three days. GitHub never retries on its own, so the platform lists GitHub's failed deliveries every hour and redelivers each one.

Deliveries can arrive out of turn. Before writing a Stripe-led record, the ingress reads Stripe's current object, so a late delivery never rolls a record back. Once the Event is written, each Action's failed steps follow [their own rules](/workflows/how-workflows-run#when-a-step-fails).

To test a Workflow without waiting for Stripe, produce the Event in test and see it arrive:

```bash
npx startup-builder trigger Dispute.opened
npx startup-builder listen 'Dispute.*'
```

[Triggers](/workflows/triggers) lists every way a Workflow starts. To send Events out to your own URL, see [outbound webhooks](/code/api/guides/webhooks).