DocsPlatformWebhooks

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

  1. You declare webhooks in manifest.json with a name and an RPC method.
  2. On deploy, the Core generates a unique 64-character hex token per webhook.
  3. External services POST to /api/v1/hooks/{token}.
  4. The Core resolves the token to your app and calls the specified RPC method on your worker with the full request data.
  5. 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