shiftlabs Start free
Menu Stripe and GitHub webhooks

Stripe and GitHub webhooks

Start a run for each Stripe event and each GitHub pull request, with every delivery's signature checked before anything runs.

What you'll build

Two workflows and two signed triggers. stripe-events runs for each refunded charge Stripe tells you about, and github-prs for each pull request opened in a repository. A delivery with a bad signature starts nothing, and a sender's retry of a delivery starts no second run.

  1. Write and deploy the workflows

    Each takes the sender's JSON as its payload.

  2. Create a trigger for each

    Stripe gives you its signing secret after you add the URL; for GitHub, you choose the secret.

  3. Point the sender at the trigger

    Then send a test delivery and find its run.

Write the workflows

A trigger passes the request body to the run as payload, parsed when it's sent as application/json, as Stripe and GitHub send it. The run gets no headers, so tell events apart by the body: Stripe's type, or GitHub's action and the object the body holds.

stripe-events.ts
import { workflow } from "shiftlabs";

type StripeEvent = {
  id: string;
  type: string;
  data: { object: { id: string; amount_refunded?: number } };
};

export default workflow<StripeEvent>(async ({ payload, step, log }) => {
  if (payload.type !== "charge.refunded") return "ignored";
  const charge = payload.data.object;
  await step.run("record refund", async () => {
    log.info("refund", { charge: charge.id, cents: charge.amount_refunded });
  });
  return charge.id;
});
github-prs.ts
import { workflow } from "shiftlabs";

type PullRequestEvent = {
  action?: string;
  pull_request?: { number: number; title: string; html_url: string };
  repository?: { full_name: string };
};

export default workflow<PullRequestEvent>(async ({ payload, step, log }) => {
  const pr = payload.pull_request;
  // The ping GitHub sends when you add the webhook has no pull_request.
  if (!pr || payload.action !== "opened") return "ignored";
  await step.run("note", async () => {
    log.info("opened", { repo: payload.repository?.full_name, pr: pr.number });
  });
  return pr.html_url;
});
shellcurl
curl -X PUT https://api.shiftlabs.org/v1/workflows/stripe-events \
  -H "authorization: Bearer $SHIFTLABS_API_KEY" \
  --data-binary @stripe-events.ts

curl -X PUT https://api.shiftlabs.org/v1/workflows/github-prs \
  -H "authorization: Bearer $SHIFTLABS_API_KEY" \
  --data-binary @github-prs.ts

# Python: the same, with -H "content-type: text/x-python" and the .py file.

Stripe

Stripe shows an endpoint's signing secret only once the endpoint exists, so create the trigger first, add its URL to Stripe, then turn signing on. The URL stays the same.

  1. Create the trigger

    POST /v1/triggers with {"workflow": "stripe-events"} answers {triggerId, url}. Until step 3, anyone with the URL can start a run, so keep it to yourself.

  2. Add the endpoint in Stripe

    Add the trigger's URL as a webhook endpoint, and choose the events to send, such as charge.refunded. Stripe then shows the endpoint's signing secret, whsec_….

  3. Turn on signing

    PATCH /v1/triggers/:id with the stripe scheme and that secret. From now on a delivery needs a valid Stripe-Signature.

shellcurl
# Answers 201 {"triggerId", "url", "signingScheme": null}.
curl -X POST https://api.shiftlabs.org/v1/triggers \
  -H "authorization: Bearer $SHIFTLABS_API_KEY" \
  -H "content-type: application/json" \
  -d '{"workflow": "stripe-events"}'

# After adding the URL in Stripe. Answers {"triggerId", "url",
# "signingScheme": "stripe"}.
curl -X PATCH https://api.shiftlabs.org/v1/triggers/<triggerId> \
  -H "authorization: Bearer $SHIFTLABS_API_KEY" \
  -H "content-type: application/json" \
  -d '{"signing": {"scheme": "stripe", "secret": "whsec_…"}}'
  • The trigger checks Stripe-Signature against the secret and refuses a timestamp more than 5 minutes off. A delivery that fails gets 401 {"error": "invalid signature"} and starts nothing.
  • Stripe signs each retry anew, so the trigger knows a retry by the event's id: the same id within 7 days answers 200 {"duplicate": true, "runId"} with the first delivery's run.
  • A body over 256 KiB gets 413. See Deploy and triggers for every answer a trigger gives.

GitHub

You choose a GitHub webhook's secret, so create the trigger signed from the start.

  1. Make a secret

    Any string of 8 to 256 characters. openssl rand -hex 32 makes one.

  2. Create the trigger

    POST /v1/triggers with the github scheme and the secret.

  3. Add the webhook in GitHub

    In the repository's Settings → Webhooks, add a webhook with the trigger's URL as the payload URL, content type application/json, and the same secret. Choose to select individual events, and pick Pull requests.

shellcurl
SECRET=$(openssl rand -hex 32)

# Answers 201 {"triggerId", "url", "signingScheme": "github"}.
curl -X POST https://api.shiftlabs.org/v1/triggers \
  -H "authorization: Bearer $SHIFTLABS_API_KEY" \
  -H "content-type: application/json" \
  -d "{\"workflow\": \"github-prs\",
       \"signing\": {\"scheme\": \"github\", \"secret\": \"$SECRET\"}}"
  • The trigger checks X-Hub-Signature-256 and needs X-GitHub-Delivery. A delivery without them, or with a signature that doesn't match, gets 401 {"error": "invalid signature"}.
  • The same X-GitHub-Delivery within 7 days answers 200 {"duplicate": true, "runId"} and starts no second run.
  • With content type application/x-www-form-urlencoded, the payload is the raw form text instead of JSON. Choose application/json.
  • Each webhook sends the events you picked to one trigger, so one workflow. For another kind of event, add a second webhook with its own trigger and workflow.

Check it worked

  • Send a test delivery

    With the endpoint added in Stripe's test mode, run stripe trigger charge.refunded with the Stripe CLI. In GitHub, open a pull request.

  • Read the sender's log

    Stripe's endpoint page and GitHub's Recent Deliveries show each delivery's answer: 202 {"runId"} for a new run, 200 {"duplicate": true, "runId"} for a repeat, and 401 when the secret on the two sides differs.

  • Find the run

    GET /v1/runs?workflow=stripe-events lists the runs, newest first, with origin trigger. GET /v1/runs/:id/timeline shows each step.

shellcurl
curl "https://api.shiftlabs.org/v1/runs?workflow=stripe-events&limit=5" \
  -H "authorization: Bearer $SHIFTLABS_API_KEY"

curl https://api.shiftlabs.org/v1/runs/<runId>/timeline \
  -H "authorization: Bearer $SHIFTLABS_API_KEY"

Reference

  • Deploy and triggers

    Signing schemes, duplicates, and every answer a trigger gives.

  • Workflows

    The run context, durable steps and retries.

  • Payload schemas

    Refuse a delivery whose body isn't the shape you expect, before it starts a run.

  • Limits

    Triggers per workspace, and the deliveries a trigger takes in 10 seconds.