verify_signature
Verify webhook signatures and diagnose failures with static header/body analysis or live endpoint testing to identify and fix verification issues.
Instructions
Verify and debug webhook signatures. Two modes: (1) Static — pass raw headers, body and secret, get exact error with fix. (2) Live — pass your endpoint URL, Tern sends a real signed test payload and diagnoses exactly why it failed.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| platform | Yes | Webhook provider platform | |
| secret | No | Webhook signing secret. Leave empty for fal.ai. | |
| headers | No | Raw request headers — for static verification mode | |
| body | No | Raw request body string — for static verification mode | |
| endpointUrl | No | Your live webhook endpoint URL — for live diagnosis mode |
Implementation Reference
- src/tools/verify-signature.ts:130-232 (handler)The primary handler function 'verifySignature' for the tool. It either performs a live diagnosis by hitting an endpoint or verifies headers/body against a secret using WebhookVerificationService.
export async function verifySignature(input: VerifySignatureInput) { if (input.endpointUrl) { try { const body = getTestPayload(input.platform) const headers = await buildSignedRequest(input.platform, input.secret, body) const response = await fetch(input.endpointUrl, { method: 'POST', headers, body, }) const responseText = await response.text().catch(() => '') if (response.ok) { return { mode: 'live_endpoint', valid: true, statusCode: response.status, endpoint: input.endpointUrl, message: `✓ Endpoint verified successfully. ${input.platform} webhook signature accepted.`, response: responseText, } } const diagnosis: Record<number, string> = { 400: 'Endpoint returned 400 — signature verification is failing. Most likely cause: raw body is being parsed before verification.', 401: 'Endpoint returned 401 — unauthorized. Secret mismatch or missing signature header.', 403: 'Endpoint returned 403 — forbidden. Signature rejected. Check your secret matches the platform dashboard.', 404: 'Endpoint returned 404 — route not found. Check your webhook URL path is correct.', 500: 'Endpoint returned 500 — server error in your handler. Check your application logs.', } return { mode: 'live_endpoint', valid: false, statusCode: response.status, endpoint: input.endpointUrl, diagnosis: diagnosis[response.status] ?? `Unexpected status ${response.status}`, response: responseText, fix: response.status === 400 || response.status === 403 ? 'Add raw body parsing before your verification step. See tern.hookflo.com for framework-specific guides.' : null, } } catch (error) { return { mode: 'live_endpoint', valid: false, error: (error as Error).message, diagnosis: 'Could not reach endpoint. Check the URL is publicly accessible and your server is running.', } } } if (!input.headers || !input.body) { return { valid: false, error: 'Provide either headers + body for static verification, or endpointUrl for live diagnosis.', } } try { const request = new Request('https://tern-mcp.local/webhook', { method: 'POST', headers: input.headers, body: input.body, }) const result = await WebhookVerificationService.verifyWithPlatformConfig( request, input.platform, input.secret, ) if (result.isValid) { return { mode: 'static', valid: true, platform: result.platform, eventId: result.eventId, payload: result.payload, message: `✓ Signature verified for ${input.platform}`, } } return { mode: 'static', valid: false, platform: result.platform, errorCode: result.errorCode, error: result.error, explanation: ERROR_EXPLANATIONS[result.errorCode ?? ''] ?? 'Unknown error. Check secret and headers.', debugTips: DEBUG_TIPS[result.errorCode ?? ''] ?? [], } } catch (error) { return { mode: 'static', valid: false, error: (error as Error).message, explanation: 'Verification threw unexpected error. Check your inputs.', } } } - src/tools/verify-signature.ts:4-27 (schema)Zod schema defining the input parameters for the 'verify_signature' tool.
export const verifySignatureSchema = z.object({ platform: z.enum([ 'stripe', 'github', 'clerk', 'shopify', 'polar', 'workos', 'dodopayments', 'paddle', 'lemonsqueezy', 'gitlab', 'sentry', 'grafana', 'doppler', 'sanity', 'falai', 'replicateai', ]).describe('The webhook provider platform'), secret: z.string() .optional() .default('') .describe('Webhook signing secret. Leave empty for fal.ai (auto JWKS)'), headers: z.record(z.string()) .optional() .describe('Raw request headers as key-value object'), body: z.string() .optional() .describe('Raw request body as string — must be raw, not parsed JSON'), endpointUrl: z.string() .optional() .describe('Your webhook endpoint URL. If provided, Tern sends a real signed test payload and diagnoses the response.'), }) export type VerifySignatureInput = z.infer<typeof verifySignatureSchema>