Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.signa.so/llms.txt

Use this file to discover all available pages before exploring further.

What you’ll build

Signa webhook ─► Cloudflare Worker ─► Slack incoming webhook
                  (verify signature)   (channel)
Two Slack-side requirements: Two Signa-side requirements:
  • A Signa API key (for creating the endpoint via the API).
  • A Signa watch already created — see Watches.

Cloudflare Worker

Save as worker.js:
import { verifyWebhookSignature } from '@signa-so/sdk';

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });

    const body = await req.text();
    const ok = verifyWebhookSignature(
      {
        'webhook-id': req.headers.get('webhook-id') ?? '',
        'webhook-timestamp': req.headers.get('webhook-timestamp') ?? '',
        'webhook-signature': req.headers.get('webhook-signature') ?? '',
      },
      body,
      env.SIGNA_WEBHOOK_SECRET,
    );
    if (!ok) return new Response('invalid signature', { status: 401 });

    const event = JSON.parse(body);
    if (event.type !== 'alert.created') return new Response('ok', { status: 200 });

    const alert = event.data;
    const slackPayload = {
      text: alertToSlackMessage(alert),
    };

    await fetch(env.SLACK_WEBHOOK_URL, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify(slackPayload),
    });

    return new Response('ok', { status: 200 });
  },
};

interface Env {
  SIGNA_WEBHOOK_SECRET: string;
  SLACK_WEBHOOK_URL: string;
}

function alertToSlackMessage(alert: Record<string, unknown>): string {
  const sev = alert.severity as string;
  const emoji = sev === 'critical' ? ':rotating_light:' : sev === 'high' ? ':warning:' : ':bell:';
  const window = alert.opposition_window_status
    ? ` (window: ${alert.opposition_window_status})`
    : '';
  return [
    `${emoji} *Signa alert* — ${alert.event_type}${window}`,
    `Watch: \`${alert.watch_id}\``,
    `Trademark: \`${alert.trademark_id}\``,
    alert.must_act_by ? `Action by: *${alert.must_act_by}*` : null,
  ]
    .filter(Boolean)
    .join('\n');
}
Customer code may want to enrich the message with a human-readable watch name via GET /v1/watches/{id}; the alert.created payload itself only carries the watch_id. Set the env vars:
wrangler secret put SIGNA_WEBHOOK_SECRET
wrangler secret put SLACK_WEBHOOK_URL
wrangler deploy
Register the Worker URL with Signa:
import Signa from '@signa-so/sdk';
const signa = new Signa({ api_key: process.env.SIGNA_API_KEY });

const wh = await signa.webhooks.create({
  url: 'https://signa-slack.<your-account>.workers.dev/',
  description: 'Slack #legal-alerts',
  enabled_events: ['alert.created'],
});
console.log('Set SIGNA_WEBHOOK_SECRET to:', wh.secret);
Copy wh.secret into the Worker as SIGNA_WEBHOOK_SECRET.

AWS Lambda + API Gateway alternative

import { verifyWebhookSignature } from '@signa-so/sdk';
import type { APIGatewayProxyHandlerV2 } from 'aws-lambda';

export const handler: APIGatewayProxyHandlerV2 = async (event) => {
  const body = event.body ?? '';
  const ok = verifyWebhookSignature(
    {
      'webhook-id': event.headers['webhook-id'] ?? '',
      'webhook-timestamp': event.headers['webhook-timestamp'] ?? '',
      'webhook-signature': event.headers['webhook-signature'] ?? '',
    },
    body,
    process.env.SIGNA_WEBHOOK_SECRET!,
  );
  if (!ok) return { statusCode: 401, body: 'invalid signature' };

  const payload = JSON.parse(body);
  if (payload.type === 'alert.created') {
    await fetch(process.env.SLACK_WEBHOOK_URL!, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ text: format(payload.data) }),
    });
  }
  return { statusCode: 200, body: 'ok' };
};
API Gateway: route a POST → Lambda. Set SIGNA_WEBHOOK_SECRET and SLACK_WEBHOOK_URL as Lambda env vars.

Tips

  • Slack rate-limits incoming webhooks. For high-volume watches, batch with a 30s buffer or fan out to a topic and let multiple webhooks consume.
  • Slack truncates messages over 40k chars — keep formatting compact.
  • For threading replies (e.g. “this watch matched 3 marks today”), use the Slack Web API rather than incoming webhooks; the same verifier pattern still applies.