# Webhook workflow examples

Complete workflows for inbound contacts, Discord app events and WhatsApp messages.

[Deploy your workflow](/deploy)

[Start for free](https://app.whenever.dev/)

## Announce new contacts in Slack

A plain inbound webhook, for any service that can POST JSON to a URL.

```ts
import {
  defineStep,
  defineWorkflow,
  NonRetryableError,
  webhook,
  type WorkflowContext,
  type WorkflowManifest,
} from '@wix/whenever-workflow-sdk';

export const manifest: WorkflowManifest = {
  name: 'Announce new contacts in Slack',
  triggers: [webhook({ key: 'contact-created', name: 'New contact' })],
};

type ContactInput = Record<string, unknown> | null | undefined;

export const announceContact = defineStep(
  'Post new contact to Slack',
  async (ctx: WorkflowContext<ContactInput>, email: string) => {
    const result = await ctx.integrations.http.post({
      url: 'https://slack.com/api/chat.postMessage',
      headers: { Authorization: `Bearer ${ctx.secrets.SLACK_BOT_TOKEN}` },
      body: { channel: ctx.config.SLACK_CHANNEL_ID, text: `New contact: ${email}` },
    });
    const body = result.body;
    if (typeof body !== 'object' || body === null || !('ok' in body) || body.ok !== true) {
      throw new NonRetryableError('Slack did not confirm the notification');
    }
    return result;
  },
);

export default defineWorkflow<
  ContactInput,
  Awaited<ReturnType<typeof announceContact>> | { ignored: string }
>(async (ctx) => {
  const input = ctx.input;
  // The endpoint is unverified, so anything can arrive: accept only the shape you expect.
  if (input === null || input === undefined) return { ignored: 'empty delivery' };
  const data = input.data;
  if (
    typeof data !== 'object' ||
    data === null ||
    !('contactEmail' in data) ||
    typeof data.contactEmail !== 'string' ||
    data.contactEmail.trim() === ''
  ) {
    throw new NonRetryableError('The delivery carries no data.contactEmail');
  }
  return await announceContact(ctx, data.contactEmail);
});
```

`webhook({ key })` checks no signature, so treat every body as untrusted: narrow it before reading a field, and ignore what you do not expect. The key is the endpoint's identity; keep it fixed and the URL stays the same across edits.

## Log Discord app installs

A sender helper verifies each delivery, and the run branches on the sender's own event name.

```ts
import {
  defineStep,
  defineWorkflow,
  type WorkflowContext,
  type WorkflowManifest,
} from '@wix/whenever-workflow-sdk';
import { discordAppEventsWebhook } from '@wix/whenever-workflow-sdk/webhooks';

type AuthorizationInput = {
  application_id: string;
  event: {
    type: 'APPLICATION_AUTHORIZED';
    timestamp: string;
    data: {
      user: { id: string; username: string };
      scopes: string[];
      guild?: { id: string; name: string };
    };
  };
};

type Authorization = {
  user: { id: string; username: string };
  server: string | null;
  scopes: string[];
};

export const manifest: WorkflowManifest = {
  name: 'Log Discord app installs',
  triggers: [discordAppEventsWebhook({ key: 'discord-app-events' })],
};

export const logAuthorization = defineStep(
  'Log Discord app authorization',
  async (ctx: WorkflowContext<AuthorizationInput>): Promise<Authorization> => {
    const { user, guild, scopes } = ctx.input.event.data;
    const authorization = {
      user: { id: user.id, username: user.username },
      server: guild?.name ?? null,
      scopes,
    };
    ctx.log('Discord app authorized', authorization);
    return authorization;
  },
);

export default defineWorkflow<AuthorizationInput, Authorization | { ignored: string }>(
  async (ctx) => {
    if (ctx.trigger.eventType !== 'APPLICATION_AUTHORIZED')
      return { ignored: 'not an authorization' };
    return await logAuthorization(ctx);
  },
);
```

`discordAppEventsWebhook` comes from `@wix/whenever-workflow-sdk/webhooks`. The platform checks Discord's signature with the app's public key from Setup before the run starts, and `ctx.trigger.eventType` carries Discord's event name. A workflow that only reads and logs needs no connected product at all.

## Reply on WhatsApp

Answers every incoming WhatsApp message with a quote of the day, through the Cloud API.

```ts
import {
  defineStep,
  defineWorkflow,
  type WorkflowContext,
  type WorkflowManifest,
} from '@wix/whenever-workflow-sdk';
import { metaAppWhatsAppWebhook } from '@wix/whenever-workflow-sdk/webhooks';

type WhatsAppInput = {
  object: 'whatsapp_business_account';
  entry: Array<{
    id: string;
    changes: Array<{
      field: 'messages';
      value: {
        metadata: { display_phone_number: string; phone_number_id: string };
        messages?: Array<{ from: string; id: string; type: string; text?: { body: string } }>;
      };
    }>;
  }>;
};

export const manifest: WorkflowManifest = {
  name: 'WhatsApp quote of the day',
  triggers: [metaAppWhatsAppWebhook({ key: 'whatsapp-messages' })],
};

const quotes = [
  'Small steps still take you somewhere.',
  'Give your attention to what you can grow today.',
  'A little progress is a reason to keep going.',
  'Rest is part of moving forward.',
];

export const replyWithQuote = defineStep(
  "Reply with today's quote",
  async (
    ctx: WorkflowContext<WhatsAppInput>,
    to: string,
    phoneNumberId: string,
    messageId: string,
    quote: string,
  ) => {
    return await ctx.integrations.http.post({
      url: `https://graph.facebook.com/${ctx.config.GRAPH_API_VERSION}/${phoneNumberId}/messages`,
      headers: { Authorization: `Bearer ${ctx.secrets.WHATSAPP_ACCESS_TOKEN}` },
      body: {
        messaging_product: 'whatsapp',
        to,
        type: 'text',
        text: { body: `Quote of the day: “${quote}”` },
        context: { message_id: messageId },
      },
    });
  },
);

type Receipt = Awaited<ReturnType<typeof replyWithQuote>>;

export default defineWorkflow<WhatsAppInput, { replies: Receipt[] }>(async (ctx) => {
  const quote = quotes[Math.floor(ctx.now() / 86_400_000) % quotes.length]!;
  const replies: Receipt[] = [];
  for (const entry of ctx.input.entry) {
    for (const change of entry.changes) {
      for (const message of change.value.messages ?? []) {
        replies.push(
          await replyWithQuote(
            ctx,
            message.from,
            change.value.metadata.phone_number_id,
            message.id,
            quote,
          ),
        );
      }
    }
  }
  return { replies };
});
```

`metaAppWhatsAppWebhook` runs only for deliveries that carry messages; status receipts never start a run. The reply goes out through `http.post` with the access token from `ctx.secrets`, and the run returns Meta's receipt for each reply, which records that Meta accepted the message, not that it was delivered.

## Before you publish

Read [what every example assumes](/docs/examples#what-every-example-assumes), the [webhook reference](/docs/webhooks) and [runtime limits and responsibilities](/docs/runtime-limits). Delivered events start executions; a delivery can contain several messages, so do not assume one message always equals one run.
