stack/docs-mintlify/docs/apps/webhooks.mdx
Madison 13fccd32b6
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
DB migration compat / Check if migrations changed (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests (Local Emulator) / E2E Tests (Local Emulator, Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Lint & build / lint_and_build (latest) (push) Has been cancelled
Dev Environment Test With Custom Base Port / restart-dev-and-test-with-custom-base-port (push) Has been cancelled
Dev Environment Test / restart-dev-and-test (push) Has been cancelled
Run setup tests with custom base port / setup-tests-with-custom-base-port (push) Has been cancelled
Run setup tests / setup-tests (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
DB migration compat / No migration changes (skipped) (push) Has been cancelled
Add docs-mintlify to root
2026-04-01 14:58:41 -05:00

252 lines
8.4 KiB
Plaintext

---
title: "Webhooks"
description: "Receive real-time updates when events occur in your Stack project"
icon: "webhook"
---
Webhooks are a powerful way to keep your backend in sync with Stack. They allow you to receive real-time updates when events occur in your Stack project, such as when a user or team is created, updated, or deleted.
For more information and a list of all webhooks, please refer to the [webhook API reference](/api/webhooks/users/user-created).
## Setting up webhooks
In the Stack dashboard, you can create a webhook endpoint in the "Webhooks" section. After creating this endpoint with your server URL, you will start receiving POST requests with a JSON payload at that endpoint. The event payload will look something like this:
```json
{
"type": "team.created",
"data": {
"id": "2209422a-eef7-4668-967d-be79409972c5",
"display_name": "My Team",
...
}
}
```
## Testing webhooks locally
You can use services like [Svix Playground](https://www.svix.com/play/) or [Webhook.site](https://webhook.site/) to test the receiving of webhooks or relay them to your local development environment.
## Verifying webhooks
To ensure the webhook is coming from Stack (and not from a malicious actor) and is not prone to replay attacks, you should verify the request.
Stack signs the webhook payload with a secret key that you can find in the endpoint details on the dashboard. You can verify the signature using the Svix client library. Check out the [Svix documentation](https://docs.svix.com/receiving/verifying-payloads/how) for instructions on how to verify the signature in JavaScript, Python, Ruby, and other languages. Here are example handlers across the supported frameworks:
<CodeGroup dropdown>
```tsx Next.js
// app/api/webhooks/stack/route.ts
import { Webhook } from "svix";
export async function POST(request: Request) {
const secret = process.env.STACK_WEBHOOK_SECRET!;
const payload = await request.text();
const headers = {
"svix-id": request.headers.get("svix-id") ?? "",
"svix-timestamp": request.headers.get("svix-timestamp") ?? "",
"svix-signature": request.headers.get("svix-signature") ?? "",
};
const wh = new Webhook(secret);
// Throws on error, returns the verified content on success.
const verifiedPayload = wh.verify(payload, headers);
return Response.json({ ok: true, type: verifiedPayload.type });
}
```
```tsx React
// server/webhooks.ts
// Webhooks must be received on a server, even if your frontend is a React app.
import express from "express";
import { Webhook } from "svix";
const app = express();
app.use("/api/webhooks/stack", express.text({ type: "application/json" }));
app.post("/api/webhooks/stack", (req, res) => {
const wh = new Webhook(process.env.STACK_WEBHOOK_SECRET!);
const verifiedPayload = wh.verify(req.body, {
"svix-id": req.header("svix-id") ?? "",
"svix-timestamp": req.header("svix-timestamp") ?? "",
"svix-signature": req.header("svix-signature") ?? "",
});
res.json({ ok: true, type: verifiedPayload.type });
});
```
```javascript Express
import express from "express";
import { Webhook } from "svix";
const app = express();
app.use("/api/webhooks/stack", express.text({ type: "application/json" }));
app.post("/api/webhooks/stack", (req, res) => {
const wh = new Webhook(process.env.STACK_WEBHOOK_SECRET);
const verifiedPayload = wh.verify(req.body, {
"svix-id": req.header("svix-id") ?? "",
"svix-timestamp": req.header("svix-timestamp") ?? "",
"svix-signature": req.header("svix-signature") ?? "",
});
res.json({ ok: true, type: verifiedPayload.type });
});
```
```javascript Node.js
import { createServer } from "node:http";
import { Webhook } from "svix";
createServer(async (req, res) => {
if (req.method !== "POST" || req.url !== "/api/webhooks/stack") {
res.writeHead(404).end();
return;
}
const payload = await new Promise((resolve, reject) => {
let body = "";
req.setEncoding("utf8");
req.on("data", (chunk) => {
body += chunk;
});
req.on("end", () => resolve(body));
req.on("error", reject);
});
const wh = new Webhook(process.env.STACK_WEBHOOK_SECRET);
const verifiedPayload = wh.verify(payload, {
"svix-id": req.headers["svix-id"] ?? "",
"svix-timestamp": req.headers["svix-timestamp"] ?? "",
"svix-signature": req.headers["svix-signature"] ?? "",
});
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true, type: verifiedPayload.type }));
}).listen(3000);
```
```javascript Vanilla JavaScript
// server.js
// Browser-only JavaScript apps still need a server endpoint to receive webhooks.
import { createServer } from "node:http";
import { Webhook } from "svix";
createServer(async (req, res) => {
if (req.method !== "POST" || req.url !== "/api/webhooks/stack") {
res.writeHead(404).end();
return;
}
const payload = await new Promise((resolve, reject) => {
let body = "";
req.setEncoding("utf8");
req.on("data", (chunk) => {
body += chunk;
});
req.on("end", () => resolve(body));
req.on("error", reject);
});
const wh = new Webhook(process.env.STACK_WEBHOOK_SECRET);
const verifiedPayload = wh.verify(payload, {
"svix-id": req.headers["svix-id"] ?? "",
"svix-timestamp": req.headers["svix-timestamp"] ?? "",
"svix-signature": req.headers["svix-signature"] ?? "",
});
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true, type: verifiedPayload.type }));
}).listen(3000);
```
```python Django
from django.conf import settings
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from svix.webhooks import Webhook
@csrf_exempt
def stack_webhook(request):
wh = Webhook(settings.STACK_WEBHOOK_SECRET)
verified_payload = wh.verify(
request.body.decode("utf-8"),
{
"svix-id": request.headers["svix-id"],
"svix-timestamp": request.headers["svix-timestamp"],
"svix-signature": request.headers["svix-signature"],
},
)
return JsonResponse({"ok": True, "type": verified_payload["type"]})
```
```python FastAPI
from fastapi import FastAPI, Request
from svix.webhooks import Webhook
app = FastAPI()
@app.post("/api/webhooks/stack")
async def stack_webhook(request: Request):
payload = await request.body()
wh = Webhook(STACK_WEBHOOK_SECRET)
verified_payload = wh.verify(
payload.decode("utf-8"),
{
"svix-id": request.headers["svix-id"],
"svix-timestamp": request.headers["svix-timestamp"],
"svix-signature": request.headers["svix-signature"],
},
)
return {"ok": True, "type": verified_payload["type"]}
```
```python Flask
from flask import Flask, jsonify, request
from svix.webhooks import Webhook
app = Flask(__name__)
@app.post("/api/webhooks/stack")
def stack_webhook():
wh = Webhook(STACK_WEBHOOK_SECRET)
verified_payload = wh.verify(
request.get_data(as_text=True),
{
"svix-id": request.headers["svix-id"],
"svix-timestamp": request.headers["svix-timestamp"],
"svix-signature": request.headers["svix-signature"],
},
)
return jsonify({"ok": True, "type": verified_payload["type"]})
```
</CodeGroup>
If you do not want to install the Svix client library or are using a language that is not supported, you can [verify the signature manually](https://docs.svix.com/receiving/verifying-payloads/how-manual).
## Event types
Please refer to the webhook endpoint API reference for more details on the available event types and their payload structures.
- [user.created](/api/webhooks/users/user-created)
- [user.updated](/api/webhooks/users/user-updated)
- [user.deleted](/api/webhooks/users/user-deleted)
- [team.created](/api/webhooks/teams/team-created)
- [team.updated](/api/webhooks/teams/team-updated)
- [team.deleted](/api/webhooks/teams/team-deleted)
- [team\_membership.created](/api/webhooks/teams/team-membership-created)
- [team\_membership.deleted](/api/webhooks/teams/team-membership-deleted)
- [team\_permission.created](/api/webhooks/teams/team-permission-created)
- [team\_permission.deleted](/api/webhooks/teams/team-permission-deleted)
## Examples
Some members of the community have shared their webhook implementations. For example, [here is an example by Clark Gredona](https://gist.github.com/clarkg/56ffad44949826ae3efe0a431b6021c4) that validates the Webhook schema and update a database user.