Webhooks
Receive HTTP callbacks from external services. Declare a webhook in your manifest, deploy, and your app gets a unique URL that routes incoming requests to your backend worker.
How it works
- You declare webhooks in
manifest.jsonwith a name and an RPC method. - On deploy, the Core generates a unique 64-character hex token per webhook.
- External services POST to
/api/v1/hooks/{token}. - The Core resolves the token to your app and calls the specified RPC method on your worker with the full request data.
- Your handler's return value is sent back as the HTTP response to the calling service.
No authentication header is required on incoming requests. The token itself authenticates the caller.
Manifest declaration
Add a webhooks array to your manifest.json. Two formats are supported:
Full format (name + RPC method to call):
{
"webhooks": [
{ "name": "stripe", "method": "onStripePayment" },
{ "name": "github", "method": "onGithubPush" }
]
}
Simple format (name only, used primarily by integrations):
{
"webhooks": ["stripe", "github"]
}
| Field | Required | Default | Description |
|---|---|---|---|
name |
Yes | Unique identifier for this webhook within the app. | |
method |
No | "POST" |
RPC method invoked on your backend when a request arrives. For applications, always specify the method explicitly. |
You can mix simple and full formats in the same array. For regular applications, use the full format with an explicit method name.
Token generation
Each webhook gets a unique 64-character hex token (32 random bytes, hex-encoded). Tokens are:
- Generated on first deploy when the webhook is created.
- Stable across re-deploys as long as the webhook name remains in the manifest.
- Unique across all apps (enforced by UNIQUE constraint on token column).
Removing a webhook from the manifest deletes it and its token. Re-adding it generates a new token.
Ingress endpoint
POST /api/v1/hooks/{token}
No Authorization header required. The token in the URL authenticates the request.
The Core routes the request to your backend's specified RPC method with these parameters:
| Parameter | Type | Description |
|---|---|---|
name |
string | Webhook name (as declared in the manifest). |
headers |
object | All HTTP headers from the incoming request as key-value pairs. |
body |
any | Parsed JSON body. If the body is not valid JSON, it is passed as a string. |
rawBody |
string | Base64-encoded raw request body. Use this for signature verification. |
Example incoming call:
curl -X POST https://your-core.rootcx.com/api/v1/hooks/02b0bd3cf466...64chars \
-H "Content-Type: application/json" \
-H "Stripe-Signature: t=1234,v1=abc..." \
-d '{"event": "payment.completed", "amount": 4999}'
Your handler receives:
{
"name": "stripe",
"headers": { "content-type": "application/json", "stripe-signature": "t=1234,v1=abc..." },
"body": { "event": "payment.completed", "amount": 4999 },
"rawBody": "eyJldmVudCI6ICJwYXltZW50LmNvbXBsZXRlZCIsICJhbW91bnQiOiA0OTk5fQ=="
}
Worker handler
Your backend receives webhook requests as standard RPC calls. The method name is what you declared in the manifest:
serve({
onStripePayment: async (params, caller, ctx) => {
// params.name - "stripe"
// params.headers - HTTP headers
// params.body - parsed JSON (or string)
// params.rawBody - base64-encoded raw body
await ctx.collection("payment_events").insert({
source: params.name,
payload: params.body,
received_at: new Date().toISOString(),
});
return { ok: true };
},
});
The return value of your handler is sent back as the HTTP response to the external service.
Signature verification
For production webhooks (Stripe, GitHub, etc.), verify the signature using rawBody:
import { createHmac } from "crypto";
onStripePayment: async (params, caller, ctx) => {
const sig = params.headers["stripe-signature"];
const raw = Buffer.from(params.rawBody, "base64");
const expected = createHmac("sha256", STRIPE_SECRET)
.update(raw)
.digest("hex");
if (sig !== expected) {
return { error: "invalid signature" };
}
// Process verified payload...
}
The rawBody (base64-encoded) preserves the exact bytes sent by the external service, which is necessary for signature verification. The parsed body field may have different whitespace or key ordering.
Retrieve webhook URLs
After deploy, retrieve your webhook URLs and tokens:
GET /api/v1/apps/{appId}/webhooks
Requires app:{appId}:webhook.read permission.
Response:
[
{
"id": "uuid",
"name": "stripe",
"method": "onStripePayment",
"token": "02b0bd3cf466...64chars",
"url": "/api/v1/hooks/02b0bd3cf466...",
"createdAt": "2024-12-01T08:38:31Z"
}
]
The url field gives you the relative path. Prepend your Core's base URL to get the full webhook URL to register with external services.
Sync behavior on deploy
On every deploy, the Core syncs webhooks to match the manifest:
| Scenario | What happens |
|---|---|
| New webhook name in manifest | Created with a new token |
| Existing webhook name, same or different method | Method updated, token preserved |
| Webhook name removed from manifest | Deleted (token invalidated) |
Integration webhooks
Integrations use a slightly different webhook mechanism. Instead of declaring webhook names that map to custom RPC methods, integration webhooks route to the __webhook RPC method with platform config injected. See Build an Integration for details.
Technical details
| Property | Value |
|---|---|
| Token length | 64 characters (32 random bytes, hex-encoded) |
| Token uniqueness | UNIQUE constraint in PostgreSQL |
| Table | rootcx_system.webhooks (id, app_id, name, method, token, created_at) |
| Constraint | UNIQUE(app_id, name) |
| Ingress path | POST /api/v1/hooks/{token} |
| Auth on ingress | None (token authenticates) |
| Response | Handler return value sent as JSON response |
| Body parsing | JSON if valid, otherwise raw string |
| Raw body encoding | Base64 (standard, with padding) |
API endpoints summary
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/v1/apps/{appId}/webhooks |
app:{appId}:webhook.read |
List webhooks with tokens and URLs |
| POST | /api/v1/hooks/{token} |
None (token auth) | Ingress endpoint for external services |