---
title: "Declare a Webhook on an app"
method: POST
path: "/api/v1/apps/{appId}/webhooks"
---

# Declare a Webhook on an app

`POST /api/v1/apps/{appId}/webhooks`

## Request

- Base URL: `https://charm.ing`
- URL: `https://charm.ing/api/v1/apps/{appId}/webhooks`
- Auth: HTTP bearer

Session-gated; requires `app:write`. Declares one of the app’s ops as a Webhook target an external sender can invoke by POSTing to the returned `url` with the returned `secret` (shown once). Unlike a Routine, the op may declare required input: the sender supplies the body. Rejects a duplicate (app, op) pair and enforces a per-app and a per-owner cap from the app owner's plan, which for a team app is the team's plan rather than the caller's. `limits` on `GET /api/v1/webhooks` reports the caller's own plan, so it matches only their personal apps. The free defaults are 3 per app and 25 per owner.

## Request body

- object
  - `op` string, required
  - `name` string

## Response `201`

Webhook created. `secret` appears only in this response.

- WebhookCreatedResponse
  - `ok` true, required
  - `webhook` Webhook, required: A declared op an external sender invokes by POSTing to `url` with the Webhook secret. The body is the op’s input.
    - `id` string, required: Public id, `webhook_<uuid>` form.
    - `app_id` string, uuid, required
    - `op` string, required
    - `name` string, required: The owner’s label; defaults to the op’s title, description, or identifier.
    - `url` string, required: The delivery URL an external sender POSTs to.
    - `secret_prefix` string, required: First characters of the secret, for matching a stored copy by eye.
    - `enabled` boolean, required
    - `disabled_reason` 'owner' | 'null', nullable, required
    - `last_run_at` string, date-time, nullable, required
    - `last_outcome` string, nullable, required
    - `last_error` object, nullable, required
      - `kind` string, required
      - `message` string, required
    - `consecutive_failures` integer, required
  - `secret` string, required: The `chrm_hook_*` secret. Shown once; only its hash is stored.
  - `target` WebhookTarget, required: The target op’s contract: the body a sender must POST (as JSON or form fields) and the value it gets back, read from the app’s active code.
    - `op` string, required
    - `input_schema` object, nullable, required
    - `output_schema` object, nullable, required

## Other responses

- `400`: Invalid body or app id.
- `401`: Sign-in required.
- `403`: The caller cannot manage this app.
- `404`: App not found, or the op is not declared on it.
- `409`: A Webhook already exists for this (app, op) pair.
- `429`: Per-app or per-owner Webhook cap reached.

---

[API](https://skmtc.dev/charm/apis/charming-api.md) · [All operations](https://skmtc.dev/charm/apis/charming-api/llms.txt) · [OpenAPI document](https://skmtc.dev/charm/apis/charming-api/revisions/a9d06c419b3a?raw)
