{
  "schemaVersion": "1.0",
  "item": {
    "slug": "hookflo-tern",
    "name": "Hookflo Webhooks",
    "source": "tencent",
    "type": "skill",
    "category": "效率提升",
    "sourceUrl": "https://clawhub.ai/Prateek32177/hookflo-tern",
    "canonicalUrl": "https://clawhub.ai/Prateek32177/hookflo-tern",
    "targetPlatform": "OpenClaw"
  },
  "install": {
    "downloadMode": "redirect",
    "downloadUrl": "/downloads/hookflo-tern",
    "sourceDownloadUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=hookflo-tern",
    "sourcePlatform": "tencent",
    "targetPlatform": "OpenClaw",
    "installMethod": "Manual import",
    "extraction": "Extract archive",
    "prerequisites": [
      "OpenClaw"
    ],
    "packageFormat": "ZIP package",
    "includedAssets": [
      "SKILL.md",
      "evals/evals.json"
    ],
    "primaryDoc": "SKILL.md",
    "quickSetup": [
      "Download the package from Yavira.",
      "Extract the archive and review SKILL.md first.",
      "Import or place the package into your OpenClaw setup."
    ],
    "agentAssist": {
      "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
      "steps": [
        "Download the package from Yavira.",
        "Extract it into a folder your agent can access.",
        "Paste one of the prompts below and point your agent at the extracted folder."
      ],
      "prompts": [
        {
          "label": "New install",
          "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
        },
        {
          "label": "Upgrade existing",
          "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
        }
      ]
    },
    "sourceHealth": {
      "source": "tencent",
      "slug": "hookflo-tern",
      "status": "healthy",
      "reason": "direct_download_ok",
      "recommendedAction": "download",
      "checkedAt": "2026-04-30T04:41:39.971Z",
      "expiresAt": "2026-05-07T04:41:39.971Z",
      "httpStatus": 200,
      "finalUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=hookflo-tern",
      "contentType": "application/zip",
      "probeMethod": "head",
      "details": {
        "probeUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=hookflo-tern",
        "contentDisposition": "attachment; filename=\"hookflo-tern-1.0.1.zip\"",
        "redirectLocation": null,
        "bodySnippet": null,
        "slug": "hookflo-tern"
      },
      "scope": "item",
      "summary": "Item download looks usable.",
      "detail": "Yavira can redirect you to the upstream package for this item.",
      "primaryActionLabel": "Download for OpenClaw",
      "primaryActionHref": "/downloads/hookflo-tern"
    },
    "validation": {
      "installChecklist": [
        "Use the Yavira download entry.",
        "Review SKILL.md after the package is downloaded.",
        "Confirm the extracted package contains the expected setup assets."
      ],
      "postInstallChecks": [
        "Confirm the extracted package includes the expected docs or setup files.",
        "Validate the skill or prompts are available in your target agent workspace.",
        "Capture any manual follow-up steps the agent could not complete."
      ]
    },
    "downloadPageUrl": "https://openagent3.xyz/downloads/hookflo-tern",
    "agentPageUrl": "https://openagent3.xyz/skills/hookflo-tern/agent",
    "manifestUrl": "https://openagent3.xyz/skills/hookflo-tern/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/hookflo-tern/agent.md"
  },
  "agentAssist": {
    "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
    "steps": [
      "Download the package from Yavira.",
      "Extract it into a folder your agent can access.",
      "Paste one of the prompts below and point your agent at the extracted folder."
    ],
    "prompts": [
      {
        "label": "New install",
        "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
      },
      {
        "label": "Upgrade existing",
        "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
      }
    ]
  },
  "documentation": {
    "source": "clawhub",
    "primaryDoc": "SKILL.md",
    "sections": [
      {
        "title": "Hookflo + Tern Webhook Skill",
        "body": "This skill covers two tightly related tools in the Hookflo ecosystem:\n\nTern (@hookflo/tern) — an open-source, zero-dependency TypeScript library for\nverifying webhook signatures. Algorithm-agnostic, supports all major platforms.\nHookflo — a hosted webhook event alerting and logging platform. Sends real-time\nSlack/email alerts when webhooks fire. No code required on their end; you point your\nprovider at Hookflo's URL and configure alerts in the dashboard."
      },
      {
        "title": "Mental Model",
        "body": "Incoming Webhook Request\n        │\n        ▼\n  [Tern] verify signature  ←── your server/edge function\n        │\n    isValid?\n        │\n   yes  │  no\n        │──────► 400 / reject\n        │\n        ▼\n  process payload\n        │\n  (optionally forward to)\n        ▼\n  [Hookflo] alert + log\n  Slack / Email / Dashboard\n\nUse Tern when you need programmatic signature verification in your own code.\nUse Hookflo when you want no-code / low-code alerting and centralized event logs.\nThey can be used together or independently."
      },
      {
        "title": "Installation",
        "body": "npm install @hookflo/tern\n\nNo other dependencies required. Full TypeScript support."
      },
      {
        "title": "Core API",
        "body": "WebhookVerificationService.verify(request, config)\n\nThe primary method. Returns a WebhookVerificationResult.\n\nimport { WebhookVerificationService } from '@hookflo/tern';\n\nconst result = await WebhookVerificationService.verify(request, {\n  platform: 'stripe',\n  secret: process.env.STRIPE_WEBHOOK_SECRET!,\n  toleranceInSeconds: 300, // replay attack protection window (optional, default 300)\n});\n\nif (result.isValid) {\n  console.log('Verified payload:', result.payload);\n  console.log('Metadata:', result.metadata); // timestamp, id, etc.\n} else {\n  console.error('Rejected:', result.error);\n  // return 400\n}\n\nWebhookVerificationService.verifyWithPlatformConfig(request, platform, secret, tolerance?)\n\nShorthand that accepts just a platform name + secret.\n\nconst result = await WebhookVerificationService.verifyWithPlatformConfig(\n  request,\n  'github',\n  process.env.GITHUB_WEBHOOK_SECRET!\n);\n\nWebhookVerificationService.verifyTokenBased(request, webhookId, webhookToken)\n\nFor token-based platforms (Supabase, GitLab).\n\nconst result = await WebhookVerificationService.verifyTokenBased(\n  request,\n  process.env.SUPABASE_WEBHOOK_ID!,\n  process.env.SUPABASE_WEBHOOK_TOKEN!\n);"
      },
      {
        "title": "WebhookVerificationResult type",
        "body": "interface WebhookVerificationResult {\n  isValid: boolean;\n  error?: string;\n  platform: WebhookPlatform;\n  payload?: any;             // parsed JSON body\n  metadata?: {\n    timestamp?: string;\n    id?: string | null;\n    [key: string]: any;\n  };\n}"
      },
      {
        "title": "Built-in Platform Configs",
        "body": "PlatformAlgorithmSignature HeaderFormatstripeHMAC-SHA256stripe-signaturet={ts},v1={sig}githubHMAC-SHA256x-hub-signature-256sha256={sig}clerkHMAC-SHA256 (base64)svix-signaturev1,{sig}supabaseToken-basedcustom—gitlabToken-basedx-gitlab-token—shopifyHMAC-SHA256x-shopify-hmac-sha256rawvercelHMAC-SHA256custom—polarHMAC-SHA256custom—dodoHMAC-SHA256 (svix)webhook-signaturev1,{sig}\n\nAlways use the lowercase string name (e.g., 'stripe', 'github')."
      },
      {
        "title": "Custom Platform Configuration",
        "body": "For any provider not in the list, supply a full signatureConfig:\n\nimport { WebhookVerificationService } from '@hookflo/tern';\n\n// Standard HMAC-SHA256 with prefix\nconst result = await WebhookVerificationService.verify(request, {\n  platform: 'acmepay',\n  secret: 'your_secret',\n  signatureConfig: {\n    algorithm: 'hmac-sha256',\n    headerName: 'x-acme-signature',\n    headerFormat: 'prefixed',\n    prefix: 'sha256=',\n    payloadFormat: 'raw',\n  },\n});\n\n// Timestamped payload (signs \"{timestamp}.{body}\")\nconst result2 = await WebhookVerificationService.verify(request, {\n  platform: 'mypay',\n  secret: 'your_secret',\n  signatureConfig: {\n    algorithm: 'hmac-sha256',\n    headerName: 'x-webhook-signature',\n    headerFormat: 'raw',\n    timestampHeader: 'x-webhook-timestamp',\n    timestampFormat: 'unix',\n    payloadFormat: 'timestamped',\n  },\n});\n\n// Svix/StandardWebhooks compatible (Clerk, Dodo, etc.)\nconst result3 = await WebhookVerificationService.verify(request, {\n  platform: 'my-svix-platform',\n  secret: 'whsec_abc123...',\n  signatureConfig: {\n    algorithm: 'hmac-sha256',\n    headerName: 'webhook-signature',\n    headerFormat: 'raw',\n    timestampHeader: 'webhook-timestamp',\n    timestampFormat: 'unix',\n    payloadFormat: 'custom',\n    customConfig: {\n      payloadFormat: '{id}.{timestamp}.{body}',\n      idHeader: 'webhook-id',\n    },\n  },\n});\n\nSignatureConfig fields:\n\nalgorithm: 'hmac-sha256' | 'hmac-sha1' | 'hmac-sha512' | custom\nheaderName: the HTTP header that carries the signature\nheaderFormat: 'raw' | 'prefixed' | 'comma-separated' | 'space-separated'\nprefix: string prefix to strip before comparing (e.g. 'sha256=')\ntimestampHeader: header name for the timestamp (if any)\ntimestampFormat: 'unix' | 'iso' | 'ms'\npayloadFormat: 'raw' | 'timestamped' | 'custom'\ncustomConfig.payloadFormat: template like '{id}.{timestamp}.{body}'\ncustomConfig.idHeader: header supplying the {id} value\ncustomConfig.encoding: 'base64' if the provider base64-encodes the key"
      },
      {
        "title": "Framework Integration",
        "body": "Express.js\n\nimport express from 'express';\nimport { WebhookVerificationService } from '@hookflo/tern';\n\nconst app = express();\n\n// IMPORTANT: use raw body parser for webhook routes\napp.post(\n  '/webhooks/stripe',\n  express.raw({ type: 'application/json' }),\n  async (req, res) => {\n    const result = await WebhookVerificationService.verifyWithPlatformConfig(\n      req,\n      'stripe',\n      process.env.STRIPE_WEBHOOK_SECRET!\n    );\n\n    if (!result.isValid) {\n      return res.status(400).json({ error: result.error });\n    }\n\n    const event = result.payload;\n    // handle event.type, e.g. 'payment_intent.succeeded'\n\n    res.json({ received: true });\n  }\n);\n\nCommon mistake: Express's default json() middleware consumes and re-serializes\nthe body, breaking HMAC. Always use express.raw() on webhook endpoints.\n\nNext.js App Router (Route Handler)\n\n// app/api/webhooks/github/route.ts\nimport { NextRequest, NextResponse } from 'next/server';\nimport { WebhookVerificationService } from '@hookflo/tern';\n\nexport async function POST(req: NextRequest) {\n  const result = await WebhookVerificationService.verifyWithPlatformConfig(\n    req,\n    'github',\n    process.env.GITHUB_WEBHOOK_SECRET!\n  );\n\n  if (!result.isValid) {\n    return NextResponse.json({ error: result.error }, { status: 400 });\n  }\n\n  const event = req.headers.get('x-github-event');\n  // handle event\n\n  return NextResponse.json({ received: true });\n}\n\n// Disable body parsing so Tern gets the raw body\nexport const config = { api: { bodyParser: false } };\n\nCloudflare Workers\n\naddEventListener('fetch', (event) => {\n  event.respondWith(handleRequest(event.request));\n});\n\nasync function handleRequest(request: Request): Promise<Response> {\n  if (request.method === 'POST' && new URL(request.url).pathname === '/webhooks/clerk') {\n    const result = await WebhookVerificationService.verifyWithPlatformConfig(\n      request,\n      'clerk',\n      CLERK_WEBHOOK_SECRET\n    );\n\n    if (!result.isValid) {\n      return new Response(JSON.stringify({ error: result.error }), {\n        status: 400,\n        headers: { 'Content-Type': 'application/json' },\n      });\n    }\n\n    return new Response(JSON.stringify({ received: true }));\n  }\n\n  return new Response('Not Found', { status: 404 });\n}"
      },
      {
        "title": "Platform Manager (Advanced)",
        "body": "import { platformManager } from '@hookflo/tern';\n\n// Verify using the platform manager directly\nconst result = await platformManager.verify(request, 'stripe', 'whsec_...');\n\n// Get the config for a platform (for inspection)\nconst config = platformManager.getConfig('stripe');\n\n// Get docs/metadata for a platform\nconst docs = platformManager.getDocumentation('stripe');\n\n// Run built-in tests for a platform\nconst passed = await platformManager.runPlatformTests('stripe');"
      },
      {
        "title": "Testing",
        "body": "npm test                       # run all tests\nnpm run test:platform stripe   # test one platform\nnpm run test:all               # test all platforms"
      },
      {
        "title": "Part 2 — Hookflo (Hosted Alerting Platform)",
        "body": "Hookflo requires no library installation. The integration is:\n\nCreate a webhook endpoint in the Hookflo Dashboard → get a Webhook URL + Secret\nPoint your provider (Stripe, Supabase, Clerk, GitHub, etc.) at that URL\nConfigure Slack/email notifications in the dashboard"
      },
      {
        "title": "How to Set Up a Hookflo Integration",
        "body": "Step 1 — Go to hookflo.com/dashboard and create a new webhook.\nYou'll receive:\n\nWebhook URL — paste into your provider's webhook settings\nWebhook ID — used for token-based platforms\nSecret Token — used by Hookflo to verify incoming events\nNotification channel settings — configure Slack or email\n\nStep 2 — Set up the provider to send to that Hookflo URL:\n\nProviderWhere to paste the URLStripeDashboard → Developers → Webhooks → Add endpointSupabaseDashboard → Database → Webhooks → Create webhookClerkDashboard → Webhooks → Add endpointGitHubRepo/Org Settings → Webhooks → Add webhook\n\nStep 3 — In the Hookflo dashboard, configure:\n\nWhich event types to alert on (e.g., payment_intent.succeeded, user.created)\nNotification channels (Slack workspace/channel, email addresses)\nDigest frequency if you want batched summaries instead of per-event alerts"
      },
      {
        "title": "Hookflo Platform Docs",
        "body": "Stripe: docs.hookflo.com/webhook-platforms/stripe\nSupabase: docs.hookflo.com/webhook-platforms/supabase\nClerk: docs.hookflo.com/webhook-platforms/clerk\nGitHub: docs.hookflo.com/webhook-platforms/github\nSlack notifications: docs.hookflo.com/notification-channels/slack"
      },
      {
        "title": "Hookflo + Tern Together",
        "body": "If you want both programmatic verification (Tern) AND logging/alerting (Hookflo), use a proxy pattern:\n\n// Your server receives the webhook, verifies it with Tern, then forwards to Hookflo\napp.post('/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {\n  // 1. Verify with Tern\n  const result = await WebhookVerificationService.verifyWithPlatformConfig(\n    req, 'stripe', process.env.STRIPE_WEBHOOK_SECRET!\n  );\n  if (!result.isValid) return res.status(400).json({ error: result.error });\n\n  // 2. Process locally\n  handleStripeEvent(result.payload);\n\n  // 3. Forward to Hookflo for alerting/logging (optional)\n  await fetch(process.env.HOOKFLO_WEBHOOK_URL!, {\n    method: 'POST',\n    headers: { ...req.headers, 'Content-Type': 'application/json' },\n    body: req.body,\n  });\n\n  res.json({ received: true });\n});\n\nAlternatively, point Stripe directly at your Hookflo URL and keep Tern for a different endpoint."
      },
      {
        "title": "Raw Body Requirement",
        "body": "HMAC signatures are computed over the exact raw bytes of the request body. Any\nre-serialization (e.g., by a JSON body parser) will break verification. Always ensure:\n\nExpress: use express.raw({ type: 'application/json' }) on webhook routes\nNext.js Pages Router: set export const config = { api: { bodyParser: false } }\nNext.js App Router: Tern reads the body directly from the Request object"
      },
      {
        "title": "Replay Attack Protection",
        "body": "Always pass toleranceInSeconds (default is 300 = 5 minutes). This rejects requests\nwith timestamps too far in the past, preventing replay attacks."
      },
      {
        "title": "Secrets Management",
        "body": "Never hardcode secrets in source code\nUse environment variables: process.env.STRIPE_WEBHOOK_SECRET\nFor Cloudflare Workers: use wrangler secret put STRIPE_WEBHOOK_SECRET\nFor Vercel: add secrets in project settings"
      },
      {
        "title": "Error Responses",
        "body": "Always return HTTP 400 (not 500) for failed verification — this signals to the sender\nthat the request was rejected (not that your server crashed)."
      },
      {
        "title": "HTTPS Only",
        "body": "Webhook endpoints must use HTTPS in production. Never accept webhook traffic over HTTP."
      },
      {
        "title": "Troubleshooting",
        "body": "SymptomLikely CauseFixisValid: false, error about signatureBody was parsed before TernUse raw body parserisValid: false, error about timestampClock skew or replay attackCheck server clock; increase tolerance if devisValid: false for ClerkMissing svix headersEnsure svix-id, svix-timestamp, svix-signature are forwardedisValid: false for GitHubWrong secretRe-copy secret from GitHub Webhooks settingsTern not finding platformTypo in platform nameUse lowercase: 'stripe', 'github', 'clerk'Hookflo not receiving eventsWrong URL pastedRe-copy URL from Hookflo dashboard"
      },
      {
        "title": "Key Links",
        "body": "Tern GitHub: https://github.com/Hookflo/tern\nTern npm: https://www.npmjs.com/package/@hookflo/tern\nTern docs: https://tern.hookflo.com\nHookflo homepage: https://hookflo.com\nHookflo dashboard: https://hookflo.com/dashboard\nHookflo docs: https://docs.hookflo.com\nHookflo Discord: https://discord.com/invite/SNmCjU97nr"
      }
    ],
    "body": "Hookflo + Tern Webhook Skill\n\nThis skill covers two tightly related tools in the Hookflo ecosystem:\n\nTern (@hookflo/tern) — an open-source, zero-dependency TypeScript library for verifying webhook signatures. Algorithm-agnostic, supports all major platforms.\nHookflo — a hosted webhook event alerting and logging platform. Sends real-time Slack/email alerts when webhooks fire. No code required on their end; you point your provider at Hookflo's URL and configure alerts in the dashboard.\nMental Model\nIncoming Webhook Request\n        │\n        ▼\n  [Tern] verify signature  ←── your server/edge function\n        │\n    isValid?\n        │\n   yes  │  no\n        │──────► 400 / reject\n        │\n        ▼\n  process payload\n        │\n  (optionally forward to)\n        ▼\n  [Hookflo] alert + log\n  Slack / Email / Dashboard\n\n\nUse Tern when you need programmatic signature verification in your own code. Use Hookflo when you want no-code / low-code alerting and centralized event logs. They can be used together or independently.\n\nPart 1 — Tern (Webhook Verification Library)\nInstallation\nnpm install @hookflo/tern\n\n\nNo other dependencies required. Full TypeScript support.\n\nCore API\nWebhookVerificationService.verify(request, config)\n\nThe primary method. Returns a WebhookVerificationResult.\n\nimport { WebhookVerificationService } from '@hookflo/tern';\n\nconst result = await WebhookVerificationService.verify(request, {\n  platform: 'stripe',\n  secret: process.env.STRIPE_WEBHOOK_SECRET!,\n  toleranceInSeconds: 300, // replay attack protection window (optional, default 300)\n});\n\nif (result.isValid) {\n  console.log('Verified payload:', result.payload);\n  console.log('Metadata:', result.metadata); // timestamp, id, etc.\n} else {\n  console.error('Rejected:', result.error);\n  // return 400\n}\n\nWebhookVerificationService.verifyWithPlatformConfig(request, platform, secret, tolerance?)\n\nShorthand that accepts just a platform name + secret.\n\nconst result = await WebhookVerificationService.verifyWithPlatformConfig(\n  request,\n  'github',\n  process.env.GITHUB_WEBHOOK_SECRET!\n);\n\nWebhookVerificationService.verifyTokenBased(request, webhookId, webhookToken)\n\nFor token-based platforms (Supabase, GitLab).\n\nconst result = await WebhookVerificationService.verifyTokenBased(\n  request,\n  process.env.SUPABASE_WEBHOOK_ID!,\n  process.env.SUPABASE_WEBHOOK_TOKEN!\n);\n\nWebhookVerificationResult type\ninterface WebhookVerificationResult {\n  isValid: boolean;\n  error?: string;\n  platform: WebhookPlatform;\n  payload?: any;             // parsed JSON body\n  metadata?: {\n    timestamp?: string;\n    id?: string | null;\n    [key: string]: any;\n  };\n}\n\nBuilt-in Platform Configs\nPlatform\tAlgorithm\tSignature Header\tFormat\nstripe\tHMAC-SHA256\tstripe-signature\tt={ts},v1={sig}\ngithub\tHMAC-SHA256\tx-hub-signature-256\tsha256={sig}\nclerk\tHMAC-SHA256 (base64)\tsvix-signature\tv1,{sig}\nsupabase\tToken-based\tcustom\t—\ngitlab\tToken-based\tx-gitlab-token\t—\nshopify\tHMAC-SHA256\tx-shopify-hmac-sha256\traw\nvercel\tHMAC-SHA256\tcustom\t—\npolar\tHMAC-SHA256\tcustom\t—\ndodo\tHMAC-SHA256 (svix)\twebhook-signature\tv1,{sig}\n\nAlways use the lowercase string name (e.g., 'stripe', 'github').\n\nCustom Platform Configuration\n\nFor any provider not in the list, supply a full signatureConfig:\n\nimport { WebhookVerificationService } from '@hookflo/tern';\n\n// Standard HMAC-SHA256 with prefix\nconst result = await WebhookVerificationService.verify(request, {\n  platform: 'acmepay',\n  secret: 'your_secret',\n  signatureConfig: {\n    algorithm: 'hmac-sha256',\n    headerName: 'x-acme-signature',\n    headerFormat: 'prefixed',\n    prefix: 'sha256=',\n    payloadFormat: 'raw',\n  },\n});\n\n// Timestamped payload (signs \"{timestamp}.{body}\")\nconst result2 = await WebhookVerificationService.verify(request, {\n  platform: 'mypay',\n  secret: 'your_secret',\n  signatureConfig: {\n    algorithm: 'hmac-sha256',\n    headerName: 'x-webhook-signature',\n    headerFormat: 'raw',\n    timestampHeader: 'x-webhook-timestamp',\n    timestampFormat: 'unix',\n    payloadFormat: 'timestamped',\n  },\n});\n\n// Svix/StandardWebhooks compatible (Clerk, Dodo, etc.)\nconst result3 = await WebhookVerificationService.verify(request, {\n  platform: 'my-svix-platform',\n  secret: 'whsec_abc123...',\n  signatureConfig: {\n    algorithm: 'hmac-sha256',\n    headerName: 'webhook-signature',\n    headerFormat: 'raw',\n    timestampHeader: 'webhook-timestamp',\n    timestampFormat: 'unix',\n    payloadFormat: 'custom',\n    customConfig: {\n      payloadFormat: '{id}.{timestamp}.{body}',\n      idHeader: 'webhook-id',\n    },\n  },\n});\n\n\nSignatureConfig fields:\n\nalgorithm: 'hmac-sha256' | 'hmac-sha1' | 'hmac-sha512' | custom\nheaderName: the HTTP header that carries the signature\nheaderFormat: 'raw' | 'prefixed' | 'comma-separated' | 'space-separated'\nprefix: string prefix to strip before comparing (e.g. 'sha256=')\ntimestampHeader: header name for the timestamp (if any)\ntimestampFormat: 'unix' | 'iso' | 'ms'\npayloadFormat: 'raw' | 'timestamped' | 'custom'\ncustomConfig.payloadFormat: template like '{id}.{timestamp}.{body}'\ncustomConfig.idHeader: header supplying the {id} value\ncustomConfig.encoding: 'base64' if the provider base64-encodes the key\nFramework Integration\nExpress.js\nimport express from 'express';\nimport { WebhookVerificationService } from '@hookflo/tern';\n\nconst app = express();\n\n// IMPORTANT: use raw body parser for webhook routes\napp.post(\n  '/webhooks/stripe',\n  express.raw({ type: 'application/json' }),\n  async (req, res) => {\n    const result = await WebhookVerificationService.verifyWithPlatformConfig(\n      req,\n      'stripe',\n      process.env.STRIPE_WEBHOOK_SECRET!\n    );\n\n    if (!result.isValid) {\n      return res.status(400).json({ error: result.error });\n    }\n\n    const event = result.payload;\n    // handle event.type, e.g. 'payment_intent.succeeded'\n\n    res.json({ received: true });\n  }\n);\n\n\nCommon mistake: Express's default json() middleware consumes and re-serializes the body, breaking HMAC. Always use express.raw() on webhook endpoints.\n\nNext.js App Router (Route Handler)\n// app/api/webhooks/github/route.ts\nimport { NextRequest, NextResponse } from 'next/server';\nimport { WebhookVerificationService } from '@hookflo/tern';\n\nexport async function POST(req: NextRequest) {\n  const result = await WebhookVerificationService.verifyWithPlatformConfig(\n    req,\n    'github',\n    process.env.GITHUB_WEBHOOK_SECRET!\n  );\n\n  if (!result.isValid) {\n    return NextResponse.json({ error: result.error }, { status: 400 });\n  }\n\n  const event = req.headers.get('x-github-event');\n  // handle event\n\n  return NextResponse.json({ received: true });\n}\n\n// Disable body parsing so Tern gets the raw body\nexport const config = { api: { bodyParser: false } };\n\nCloudflare Workers\naddEventListener('fetch', (event) => {\n  event.respondWith(handleRequest(event.request));\n});\n\nasync function handleRequest(request: Request): Promise<Response> {\n  if (request.method === 'POST' && new URL(request.url).pathname === '/webhooks/clerk') {\n    const result = await WebhookVerificationService.verifyWithPlatformConfig(\n      request,\n      'clerk',\n      CLERK_WEBHOOK_SECRET\n    );\n\n    if (!result.isValid) {\n      return new Response(JSON.stringify({ error: result.error }), {\n        status: 400,\n        headers: { 'Content-Type': 'application/json' },\n      });\n    }\n\n    return new Response(JSON.stringify({ received: true }));\n  }\n\n  return new Response('Not Found', { status: 404 });\n}\n\nPlatform Manager (Advanced)\nimport { platformManager } from '@hookflo/tern';\n\n// Verify using the platform manager directly\nconst result = await platformManager.verify(request, 'stripe', 'whsec_...');\n\n// Get the config for a platform (for inspection)\nconst config = platformManager.getConfig('stripe');\n\n// Get docs/metadata for a platform\nconst docs = platformManager.getDocumentation('stripe');\n\n// Run built-in tests for a platform\nconst passed = await platformManager.runPlatformTests('stripe');\n\nTesting\nnpm test                       # run all tests\nnpm run test:platform stripe   # test one platform\nnpm run test:all               # test all platforms\n\nPart 2 — Hookflo (Hosted Alerting Platform)\n\nHookflo requires no library installation. The integration is:\n\nCreate a webhook endpoint in the Hookflo Dashboard → get a Webhook URL + Secret\nPoint your provider (Stripe, Supabase, Clerk, GitHub, etc.) at that URL\nConfigure Slack/email notifications in the dashboard\nHow to Set Up a Hookflo Integration\n\nStep 1 — Go to hookflo.com/dashboard and create a new webhook. You'll receive:\n\nWebhook URL — paste into your provider's webhook settings\nWebhook ID — used for token-based platforms\nSecret Token — used by Hookflo to verify incoming events\nNotification channel settings — configure Slack or email\n\nStep 2 — Set up the provider to send to that Hookflo URL:\n\nProvider\tWhere to paste the URL\nStripe\tDashboard → Developers → Webhooks → Add endpoint\nSupabase\tDashboard → Database → Webhooks → Create webhook\nClerk\tDashboard → Webhooks → Add endpoint\nGitHub\tRepo/Org Settings → Webhooks → Add webhook\n\nStep 3 — In the Hookflo dashboard, configure:\n\nWhich event types to alert on (e.g., payment_intent.succeeded, user.created)\nNotification channels (Slack workspace/channel, email addresses)\nDigest frequency if you want batched summaries instead of per-event alerts\nHookflo Platform Docs\nStripe: docs.hookflo.com/webhook-platforms/stripe\nSupabase: docs.hookflo.com/webhook-platforms/supabase\nClerk: docs.hookflo.com/webhook-platforms/clerk\nGitHub: docs.hookflo.com/webhook-platforms/github\nSlack notifications: docs.hookflo.com/notification-channels/slack\nHookflo + Tern Together\n\nIf you want both programmatic verification (Tern) AND logging/alerting (Hookflo), use a proxy pattern:\n\n// Your server receives the webhook, verifies it with Tern, then forwards to Hookflo\napp.post('/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {\n  // 1. Verify with Tern\n  const result = await WebhookVerificationService.verifyWithPlatformConfig(\n    req, 'stripe', process.env.STRIPE_WEBHOOK_SECRET!\n  );\n  if (!result.isValid) return res.status(400).json({ error: result.error });\n\n  // 2. Process locally\n  handleStripeEvent(result.payload);\n\n  // 3. Forward to Hookflo for alerting/logging (optional)\n  await fetch(process.env.HOOKFLO_WEBHOOK_URL!, {\n    method: 'POST',\n    headers: { ...req.headers, 'Content-Type': 'application/json' },\n    body: req.body,\n  });\n\n  res.json({ received: true });\n});\n\n\nAlternatively, point Stripe directly at your Hookflo URL and keep Tern for a different endpoint.\n\nCommon Pitfalls & Best Practices\nRaw Body Requirement\n\nHMAC signatures are computed over the exact raw bytes of the request body. Any re-serialization (e.g., by a JSON body parser) will break verification. Always ensure:\n\nExpress: use express.raw({ type: 'application/json' }) on webhook routes\nNext.js Pages Router: set export const config = { api: { bodyParser: false } }\nNext.js App Router: Tern reads the body directly from the Request object\nReplay Attack Protection\n\nAlways pass toleranceInSeconds (default is 300 = 5 minutes). This rejects requests with timestamps too far in the past, preventing replay attacks.\n\nSecrets Management\nNever hardcode secrets in source code\nUse environment variables: process.env.STRIPE_WEBHOOK_SECRET\nFor Cloudflare Workers: use wrangler secret put STRIPE_WEBHOOK_SECRET\nFor Vercel: add secrets in project settings\nError Responses\n\nAlways return HTTP 400 (not 500) for failed verification — this signals to the sender that the request was rejected (not that your server crashed).\n\nHTTPS Only\n\nWebhook endpoints must use HTTPS in production. Never accept webhook traffic over HTTP.\n\nTroubleshooting\nSymptom\tLikely Cause\tFix\nisValid: false, error about signature\tBody was parsed before Tern\tUse raw body parser\nisValid: false, error about timestamp\tClock skew or replay attack\tCheck server clock; increase tolerance if dev\nisValid: false for Clerk\tMissing svix headers\tEnsure svix-id, svix-timestamp, svix-signature are forwarded\nisValid: false for GitHub\tWrong secret\tRe-copy secret from GitHub Webhooks settings\nTern not finding platform\tTypo in platform name\tUse lowercase: 'stripe', 'github', 'clerk'\nHookflo not receiving events\tWrong URL pasted\tRe-copy URL from Hookflo dashboard\nKey Links\nTern GitHub: https://github.com/Hookflo/tern\nTern npm: https://www.npmjs.com/package/@hookflo/tern\nTern docs: https://tern.hookflo.com\nHookflo homepage: https://hookflo.com\nHookflo dashboard: https://hookflo.com/dashboard\nHookflo docs: https://docs.hookflo.com\nHookflo Discord: https://discord.com/invite/SNmCjU97nr"
  },
  "trust": {
    "sourceLabel": "tencent",
    "provenanceUrl": "https://clawhub.ai/Prateek32177/hookflo-tern",
    "publisherUrl": "https://clawhub.ai/Prateek32177/hookflo-tern",
    "owner": "Prateek32177",
    "version": "1.0.1",
    "license": null,
    "verificationStatus": "Indexed source record"
  },
  "links": {
    "detailUrl": "https://openagent3.xyz/skills/hookflo-tern",
    "downloadUrl": "https://openagent3.xyz/downloads/hookflo-tern",
    "agentUrl": "https://openagent3.xyz/skills/hookflo-tern/agent",
    "manifestUrl": "https://openagent3.xyz/skills/hookflo-tern/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/hookflo-tern/agent.md"
  }
}