> ## Documentation Index
> Fetch the complete documentation index at: https://docs.opencomputer.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Hooks (inbound)

> Pasteable, scoped URLs that send external events to an agent

Hooks send external events **to your agent**. Create a named Hook URL for each
external system, then paste it into that system's HTTP or webhook destination.
Each accepted `POST` starts one ordinary durable session on the agent's active
revision.

<Warning>
  The complete URL is a secret. Anyone with it can run this agent. It cannot
  read sessions, manage your account, or invoke another agent. Copy it when it
  is created—the complete URL cannot be retrieved again.
</Warning>

## Create a Hook URL

<CodeGroup>
  ```ts TypeScript SDK theme={null}
  const created = await oc.agents.hooks.create("agt_...", {
    name: "grafana-prod",
    expiresAt: null,
  });

  console.log(created.hookUrl); // shown once
  ```

  ```bash oc CLI theme={null}
  oc agent hook create grafana-prod --agent reviewer
  ```

  ```http REST API theme={null}
  POST https://api.opencomputer.dev/v3/agents/agt_.../hooks
  Authorization: Bearer $OPENCOMPUTER_API_KEY
  Content-Type: application/json

  { "name": "grafana-prod", "expires_at": null }
  ```
</CodeGroup>

Names match `^[a-z0-9][a-z0-9-]{0,63}$`. Expiry is optional and must be a
future RFC 3339 timestamp. An agent may have up to 20 non-revoked Hooks; expired
Hooks keep a slot until revoked. Hook lifecycle is operational credential state,
not agent revision state, so deploys and rollbacks do not copy or replace Hooks.

Hook creation is deliberately not idempotent because the secret is stored only
as a hash. If the creation response is lost, list by name, revoke the
inaccessible Hook, and create a replacement.

## Send an event

```bash theme={null}
curl "$HOOK_URL" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: alert-018f" \
  -d '{"status":"firing","service":"checkout","summary":"error rate above 5%"}'
```

The URL authenticates the request; do not add an OpenComputer API key. The
contract is exact `POST`, no query string, and either an empty body or one JSON
value. Non-empty bodies require a JSON content type and are capped at 24 KiB.
The Hook name and parsed payload become a structured `http.request` event; the
secret URL and transport headers never do.

```json theme={null}
{
  "type": "http.request",
  "actor": { "type": "trigger", "display": "grafana-prod" },
  "body": {
    "payload": {
      "status": "firing",
      "service": "checkout",
      "summary": "error rate above 5%"
    }
  }
}
```

Success returns `200 OK` only after session admission is durable:

```json theme={null}
{
  "request_id": "req_...",
  "session": { "id": "ses_...", "status": "queued" },
  "replayed": false
}
```

This is an asynchronous receipt, not the agent's output. It contains no client
token and grants no session read or steer access. Watch the session in the
dashboard or configure an outbound [webhook](/agent-sessions/webhooks) for
results.

## Common senders

Create a separate named Hook for each sender so it can be rotated or revoked
without interrupting the others.

* **Grafana:** Add a [Webhook contact point](https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier/), use the Hook URL, and keep the method as `POST`. Grafana sends its alert JSON directly. Its standard webhook does not provide a supported delivery-id header; see [Retries and duplicates](#retries-and-duplicates).
* **Zapier:** Add a [Webhooks by Zapier](https://help.zapier.com/hc/en-us/articles/8496326446989-Send-webhooks-in-Zap-workflows) `POST` action, paste the Hook URL, select a JSON payload, and map fields from earlier steps. No separate authentication field is needed because the secret is already in the URL.
* **Supabase:** Create a [Database Webhook](https://supabase.com/docs/guides/database/webhooks), select the table and `INSERT`, `UPDATE`, or `DELETE` events, choose `POST`, and paste the Hook URL. Supabase sends the row-change JSON as the payload.
* **CI:** Store the complete Hook URL as a secret and send one JSON request. Use a stable delivery id for retries of the same run:

```yaml GitHub Actions theme={null}
- name: Notify agent
  env:
    HOOK_URL: ${{ secrets.OC_AGENT_HOOK_URL }}
  run: |
    curl --fail-with-body "$HOOK_URL" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: ci-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" \
      -d "{\"repository\":\"$GITHUB_REPOSITORY\",\"sha\":\"$GITHUB_SHA\"}"
```

## Retries and duplicates

For retry-safe delivery, send `Idempotency-Key`; when absent, OpenComputer uses
the Standard Webhooks `webhook-id` header if present. A repeated identifier and
payload returns the same session. Reusing it with different JSON returns `409`.
Without either header, identical requests are separate events and create
separate sessions—OpenComputer does not guess duplicates from the body.

Grafana's standard webhook does not provide either supported delivery-id
header. Do not configure one constant idempotency key for all Grafana alerts;
later alerts would conflict or replay. Accept possible redelivery duplicates,
deduplicate their side effects, or use a provider-verified connector when that
is unacceptable.

## List, replace, and revoke

<CodeGroup>
  ```ts TypeScript SDK theme={null}
  const page = await oc.agents.hooks.list("agt_...", {
    includeRevoked: true,
  });

  await oc.agents.hooks.delete("agt_...", "hk_...");
  ```

  ```bash oc CLI theme={null}
  oc agent hooks --agent reviewer --include-revoked
  oc agent hook get grafana-prod --agent reviewer
  oc agent hook revoke grafana-prod --agent reviewer
  ```

  ```http REST API theme={null}
  GET /v3/agents/agt_.../hooks?include_revoked=true
  GET /v3/agents/agt_.../hooks/hk_...
  DELETE /v3/agents/agt_.../hooks/hk_...
  ```
</CodeGroup>

Lists contain the name, public Hook id, status, expiry, creation time, secret
last four, and revoke reason—never the usable URL. To rotate, create a new Hook,
update the external system, then revoke the old one. Revocation is irreversible
and does not affect sessions the Hook already created.

## Security, usage, and limits

Hook names record intended custody; they do not prove that a named provider sent
the request. A Hook does not validate provider signatures, challenge requests,
or raw-body semantics. Use a provider connector for those guarantees, and do
not send payloads the agent is not allowed to receive.

Rate limits reduce bursts; they are not a spend cap. Every accepted POST creates
a fresh session, so all per-session limits reset on every call. On built-in
runtimes, `limits.tokens` is checked between turns from reported usage and can
overshoot by one in-flight turn; it does not interrupt a model request. It is
not a currency limit or an aggregate Hook quota. Set agent turn/time/token
limits, monitor session usage, and revoke unexpected activity. Your
organization's credit halt is the aggregate fuse.

Flue sessions expose best-effort per-session token attribution; the agent/org
gateway remains their spend-enforcement authority. See [session usage and
limits](/agent-sessions/sessions#usage-and-limits).
