Docs

Receive webhooks

In this example, we are going to give each customer an HTTPS endpoint that receives and stores webhooks in their own deployment. A Command lets your product read the events it needs without exposing a second admin API.

This is useful when webhook payloads contain customer data that should be processed and retained in the customer's environment. GitHub, Stripe, or another sender still uses an ordinary public URL; your hosted backend does not need the KV credential or a route into the customer's network.

The two entry points serve different callers. GitHub, Stripe, or another external service sends an ordinary HTTP request. Your backend invokes a Command for a specific deployment when it needs the recorded events.

An external service sends an HTTPS webhook to a Worker in the customer environment. The Worker verifies and stores the event, then returns an HTTP response.

We will describe the Worker, public endpoint, and KV store in alien.ts. Then we will implement the webhook route and the Command in src/index.ts.

Describe the endpoint and storage

alien.ts
const events = new alien.Kv("events").build()

const api = new alien.Worker("api")
  .code({ type: "source", src: "./", toolchain: { type: "typescript" } })
  .commandsEnabled(true)
  .publicEndpoint("api")
  .link(events)
  .permissions("execution")
  .build()

.link(events) gives this Worker access to the deployment's KV resource. The execution permission profile becomes the cloud permissions for that access; the hosted control plane never receives the KV credential.

KV is a small key-value store created for each deployment. This example uses keys such as github:1724450000000, which makes recent events easy to scan by source without adding a database service.

Receive and store a webhook

src/index.ts
app.post("/webhooks/:source", async c => {
  const source = c.req.param("source")
  const body = await c.req.json()
  const key = `${source}:${Date.now()}`

  await kv("events").setJson(key, {
    source,
    body,
    receivedAt: new Date().toISOString(),
  })

  return c.json({ received: true, key })
})

The full example also defines a signing-secret input. Verify the sender's signature before storing real webhooks.

Let your product read the events

src/index.ts
command("get-events", querySchema, async ({ source, limit }) => {
  const prefix = source ? `${source}:` : ""
  return scanEvents(kv("events"), { prefix, limit })
})

Run it locally

alien init webhook-api-ts
alien dev
curl -X POST http://localhost:<port>/webhooks/github -H 'content-type: application/json' -d '{"action":"opened"}'

The port is printed by alien dev.

Read the complete source for cursor-based KV scanning, input validation, and webhook configuration.

Give each customer their own webhook endpoint

1 · Release
alien release

Publishes a version. Nothing is deployed for a customer yet.

2 · Invite
alien onboard acme-corp

Creates a deployment link for that customer.

3 · Deploy

The customer opens the link and deploys into their environment.

Each deployment receives its own HTTPS endpoint and KV store. Your control plane can query that deployment's events through Commands.

What you built

You built a customer-specific webhook receiver whose payloads are accepted and stored in the customer's cloud. Outside senders use HTTPS, while your product uses a narrow Command interface to inspect results without receiving storage credentials or exposing an administrative port.

Source: examples/webhook-api-ts.

Next: Networking and endpoints, Events and schedules.

On this page