# AnyHook code templates

Starting points for an app's code (PUT /api/v1/apps/{slug}/code, or anyhook_code_deploy over MCP). The code
runs in a sandbox on every event the app receives: `export default async function (event, { env, store })`,
where event is { method, headers, body, raw_body, delivery_id, attempt } and attempt is 0 on a test run.

- Secrets are env.NAME, set beside the code; never write a key into it.
- Throw when a call fails: AnyHook retries the delivery, and a retry of an event whose run succeeded does not run again.
- A run that fails after its call went through runs again on the retry, so key the call to the event where the
  API allows it, as the email template does with delivery_id.
- Imports from npm are installed when the code is saved.

## Slack message

Secrets: SLACK_WEBHOOK_URL

```js
// Post a message to a Slack channel.
// Secret SLACK_WEBHOOK_URL: api.slack.com/apps → your app → Incoming Webhooks
// → Add New Webhook, then copy its URL.
export default async function (event, { env }) {
  if (!env.SLACK_WEBHOOK_URL) throw new Error("Add the secret SLACK_WEBHOOK_URL");
  // Say what matters from event.body: its type, a name, an amount.
  const text = `New event: ${JSON.stringify(event.body).slice(0, 3000)}`;
  const res = await fetch(env.SLACK_WEBHOOK_URL, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ text }),
  });
  // Throwing fails this delivery, and AnyHook tries it again later.
  if (!res.ok) throw new Error(`Slack answered ${res.status}: ${await res.text()}`);
}
```

## Discord message

Secrets: DISCORD_WEBHOOK_URL

```js
// Post a message to a Discord channel.
// Secret DISCORD_WEBHOOK_URL: channel settings → Integrations → Webhooks
// → New Webhook → Copy Webhook URL.
export default async function (event, { env }) {
  if (!env.DISCORD_WEBHOOK_URL) throw new Error("Add the secret DISCORD_WEBHOOK_URL");
  const res = await fetch(env.DISCORD_WEBHOOK_URL, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      content: `New event: ${JSON.stringify(event.body).slice(0, 1900)}`, // Discord takes 2,000 characters
      allowed_mentions: { parse: [] }, // an "@everyone" in the payload pings nobody
    }),
  });
  // Throwing fails this delivery, and AnyHook tries it again later.
  if (!res.ok) throw new Error(`Discord answered ${res.status}: ${await res.text()}`);
}
```

## Telegram message

Secrets: TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID

```js
// Send a message from your Telegram bot.
// Secrets TELEGRAM_BOT_TOKEN (message @BotFather, send /newbot) and
// TELEGRAM_CHAT_ID (message your bot, open api.telegram.org/bot<token>/getUpdates
// and copy message.chat.id).
export default async function (event, { env }) {
  for (const name of ["TELEGRAM_BOT_TOKEN", "TELEGRAM_CHAT_ID"]) {
    if (!env[name]) throw new Error(`Add the secret ${name}`);
  }
  const res = await fetch(`https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/sendMessage`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      chat_id: env.TELEGRAM_CHAT_ID,
      text: `New event: ${JSON.stringify(event.body).slice(0, 4000)}`, // Telegram takes 4,096 characters
    }),
  });
  const out = await res.json().catch(() => ({}));
  // Throwing fails this delivery, and AnyHook tries it again later.
  if (!out.ok) throw new Error(`Telegram answered ${res.status}: ${out.description}`);
}
```

## Email through Resend

Secrets: RESEND_API_KEY, EMAIL_FROM, EMAIL_TO

```js
// Send an email through Resend.
// Secrets RESEND_API_KEY (resend.com/api-keys), EMAIL_FROM (an address on a
// domain you verified in Resend, such as "Alerts <alerts@example.com>") and
// EMAIL_TO (one address, or several separated by commas).
export default async function (event, { env }) {
  for (const name of ["RESEND_API_KEY", "EMAIL_FROM", "EMAIL_TO"]) {
    if (!env[name]) throw new Error(`Add the secret ${name}`);
  }
  const res = await fetch("https://api.resend.com/emails", {
    method: "POST",
    headers: {
      authorization: `Bearer ${env.RESEND_API_KEY}`,
      "content-type": "application/json",
      // If a run sends the email and then fails, its retry sends no second one:
      // Resend keeps this key for 24 hours. Test runs (attempt 0) go without it.
      ...(event.attempt > 0 && { "idempotency-key": `anyhook-${event.delivery_id}` }),
    },
    body: JSON.stringify({
      from: env.EMAIL_FROM,
      to: env.EMAIL_TO.split(",").map((address) => address.trim()),
      subject: "New event",
      text: JSON.stringify(event.body, null, 2),
    }),
  });
  // Throwing fails this delivery, and AnyHook tries it again later.
  if (!res.ok) throw new Error(`Resend answered ${res.status}: ${await res.text()}`);
}
```

## Google Sheets row

Secrets: GOOGLE_SERVICE_ACCOUNT, SHEET_ID

```js
// Add a row to a Google Sheet, as a service account: no sign-in that expires.
// Secrets GOOGLE_SERVICE_ACCOUNT, the whole JSON key file (Google Cloud console
// → IAM & Admin → Service accounts → Keys → Add key → JSON), and SHEET_ID, from
// the sheet's URL: docs.google.com/spreadsheets/d/SHEET_ID/edit. Turn on the
// Google Sheets API in the key's project, and share the sheet with the service
// account's client_email as an editor.
import { createSign } from "node:crypto";

const RANGE = "A1"; // the first sheet; "Orders!A1" for a sheet named Orders

export default async function (event, { env }) {
  for (const name of ["GOOGLE_SERVICE_ACCOUNT", "SHEET_ID"]) {
    if (!env[name]) throw new Error(`Add the secret ${name}`);
  }
  const row = [new Date().toISOString(), JSON.stringify(event.body)];
  const url = `https://sheets.googleapis.com/v4/spreadsheets/${env.SHEET_ID}/values/${encodeURIComponent(RANGE)}:append`;
  // RAW: a value that starts with "=" stays text instead of becoming a formula.
  const res = await fetch(`${url}?valueInputOption=RAW&insertDataOption=INSERT_ROWS`, {
    method: "POST",
    headers: {
      authorization: `Bearer ${await accessToken(JSON.parse(env.GOOGLE_SERVICE_ACCOUNT))}`,
      "content-type": "application/json",
    },
    body: JSON.stringify({ values: [row] }),
  });
  // Throwing fails this delivery, and AnyHook tries it again later.
  if (!res.ok) throw new Error(`Google Sheets answered ${res.status}: ${await res.text()}`);
}

// A service account signs a request for a one-hour token with its private key.
async function accessToken(key) {
  const now = Math.floor(Date.now() / 1000);
  const part = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
  const claims = {
    iss: key.client_email,
    scope: "https://www.googleapis.com/auth/spreadsheets",
    aud: "https://oauth2.googleapis.com/token",
    iat: now,
    exp: now + 3600,
  };
  const unsigned = `${part({ alg: "RS256", typ: "JWT" })}.${part(claims)}`;
  const signature = createSign("RSA-SHA256").update(unsigned).sign(key.private_key, "base64url");
  const res = await fetch("https://oauth2.googleapis.com/token", {
    method: "POST",
    body: new URLSearchParams({
      grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
      assertion: `${unsigned}.${signature}`,
    }),
  });
  const out = await res.json().catch(() => ({}));
  if (!out.access_token) throw new Error(`Google refused the service account (${res.status}): ${out.error_description ?? out.error}`);
  return out.access_token;
}
```

## Airtable record

Secrets: AIRTABLE_TOKEN, AIRTABLE_BASE_ID, AIRTABLE_TABLE

```js
// Add a record to an Airtable table.
// Secrets AIRTABLE_TOKEN (airtable.com/create/tokens, with the scope
// data.records:write and access to the base), AIRTABLE_BASE_ID (the app… part
// of the base's URL) and AIRTABLE_TABLE (the table's name, or its tbl… id).
export default async function (event, { env }) {
  for (const name of ["AIRTABLE_TOKEN", "AIRTABLE_BASE_ID", "AIRTABLE_TABLE"]) {
    if (!env[name]) throw new Error(`Add the secret ${name}`);
  }
  const res = await fetch(`https://api.airtable.com/v0/${env.AIRTABLE_BASE_ID}/${encodeURIComponent(env.AIRTABLE_TABLE)}`, {
    method: "POST",
    headers: { authorization: `Bearer ${env.AIRTABLE_TOKEN}`, "content-type": "application/json" },
    // The field names must be your table's.
    body: JSON.stringify({ fields: { Name: "New event", Notes: JSON.stringify(event.body, null, 2) } }),
  });
  // Throwing fails this delivery, and AnyHook tries it again later.
  if (!res.ok) throw new Error(`Airtable answered ${res.status}: ${await res.text()}`);
}
```

## Notion page

Secrets: NOTION_TOKEN, NOTION_DATA_SOURCE_ID

```js
// Add a page to a Notion database.
// Secrets NOTION_TOKEN (notion.so/profile/integrations → New integration, copy
// its secret, then connect it to the database: ⋯ → Connections) and
// NOTION_DATA_SOURCE_ID (the database's settings → Manage data sources →
// Copy data source ID).
export default async function (event, { env }) {
  for (const name of ["NOTION_TOKEN", "NOTION_DATA_SOURCE_ID"]) {
    if (!env[name]) throw new Error(`Add the secret ${name}`);
  }
  const res = await fetch("https://api.notion.com/v1/pages", {
    method: "POST",
    headers: {
      authorization: `Bearer ${env.NOTION_TOKEN}`,
      "content-type": "application/json",
      "notion-version": "2026-03-11",
    },
    body: JSON.stringify({
      parent: { type: "data_source_id", data_source_id: env.NOTION_DATA_SOURCE_ID },
      // "Name" is the database's title property: use your database's name for it.
      properties: { Name: { title: [{ type: "text", text: { content: "New event" } }] } },
      children: [{
        type: "code",
        code: {
          language: "json",
          rich_text: [{ type: "text", text: { content: JSON.stringify(event.body, null, 2).slice(0, 2000) } }],
        },
      }],
    }),
  });
  // Throwing fails this delivery, and AnyHook tries it again later.
  if (!res.ok) throw new Error(`Notion answered ${res.status}: ${await res.text()}`);
}
```

## Ask Claude

Secrets: ANTHROPIC_API_KEY

```js
// Ask Claude about each event: a summary here, and a label, a reply draft or
// the fields you need work the same way. The answer is the run's result, shown
// in the event log; post it on with fetch, as the Slack template does.
// Secret ANTHROPIC_API_KEY: platform.claude.com → API keys.
import Anthropic from "@anthropic-ai/sdk";

export default async function (event, { env }) {
  if (!env.ANTHROPIC_API_KEY) throw new Error("Add the secret ANTHROPIC_API_KEY");
  const client = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY });
  const message = await client.beta.messages.create({
    model: "claude-opus-5-5",
    max_tokens: 16000,
    output_config: { effort: "low" }, // a short answer; raise it for harder questions
    // If Claude declines, the API answers with a fallback model in the same call.
    betas: ["server-side-fallback-2026-07-01"],
    fallbacks: "default",
    messages: [{
      role: "user",
      content: `Summarize this webhook event in one sentence for a team chat:\n\n${JSON.stringify(event.body)}`,
    }],
  });
  if (message.stop_reason === "refusal") {
    console.warn("Claude declined this event");
    return null;
  }
  return { summary: message.content.filter((block) => block.type === "text").map((block) => block.text).join("") };
}
```

## Any HTTP API

Secrets: TARGET_URL

```js
// Send each event on to another HTTP API, in the shape it expects.
// Secrets TARGET_URL and, if the API wants one, TARGET_TOKEN.
export default async function (event, { env }) {
  if (!env.TARGET_URL) throw new Error("Add the secret TARGET_URL");
  // To act on some events only, return early: the rest count as delivered.
  // if (event.body.type !== "invoice.paid") return;
  const res = await fetch(env.TARGET_URL, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      ...(env.TARGET_TOKEN && { authorization: `Bearer ${env.TARGET_TOKEN}` }),
    },
    body: JSON.stringify({ received_at: new Date().toISOString(), data: event.body }),
  });
  // The host only: a webhook URL's path is often its secret.
  if (!res.ok) throw new Error(`${new URL(env.TARGET_URL).host} answered ${res.status}: ${await res.text()}`);
}
```
