Skip to content
The Whenever SDK is open source.Explore the SDK on GitHub
On this page

Webhook workflow examples

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

Announce new contacts in Slack

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

TypeScript
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.

TypeScript
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.

TypeScript
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, the webhook reference and runtime limits and responsibilities. Delivered events start executions; a delivery can contain several messages, so do not assume one message always equals one run.

Ready to deploy?