mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
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
252 lines
8.4 KiB
Plaintext
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.
|