From 0d1365401e65cf7384d5ac3a26a33d6dde44e4ab Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 10 Nov 2025 16:58:50 +0100 Subject: [PATCH] [Alex] ARC-542: Add egress-webhook-service openapi yaml and render it --- docs.json | 17 +- index.mdx | 83 ----- services/egress-webhook-service/openapi.yaml | 336 ++++++++++++++++++ .../openapi.yaml | 295 --------------- webhooks.mdx | 87 +++++ 5 files changed, 438 insertions(+), 380 deletions(-) create mode 100644 services/egress-webhook-service/openapi.yaml create mode 100644 webhooks.mdx diff --git a/docs.json b/docs.json index 5b3c95a..29645b6 100644 --- a/docs.json +++ b/docs.json @@ -15,16 +15,29 @@ "navigation": { "tabs": [ { - "tab": "API Reference", - "openapi": "services/external-actor-gateway-service/openapi.yaml", + "tab": "Overview", "groups": [ { "group": "Overview", "pages": [ "index" ] + }, + { + "group": "Webhooks", + "pages": [ + "webhooks" + ] } ] + }, + { + "tab": "Customer API", + "openapi": "services/external-actor-gateway-service/openapi.yaml" + }, + { + "tab": "Webhook Events", + "openapi": "services/egress-webhook-service/openapi.yaml" } ] }, diff --git a/index.mdx b/index.mdx index 47cd5c2..7da42a3 100644 --- a/index.mdx +++ b/index.mdx @@ -69,89 +69,6 @@ The API uses standard HTTP status codes and returns detailed error responses: ``` -## Webhook Signature Verification - -Webhook requests include an HMAC-SHA256 signature in the `X-Webhook-Signature-256` header that you should verify to ensure authenticity and prevent tampering. - -### Verification Process - -1. Extract the signature from the `X-Webhook-Signature-256` header (format: `sha256=`) -2. Get the raw request body before parsing -3. Calculate the expected signature using your webhook signing secret -4. Compare signatures using a timing-safe comparison function -5. Process the webhook only if signatures match - - -Always use the raw request body for verification, not the parsed JSON. Use timing-safe comparison functions to prevent timing attacks. - - -### Implementation Example - -```typescript -import crypto from 'node:crypto'; -import express from 'express'; - -function verifyWebhookSignature( - signingSecret: string, - payload: string, - signature: string -): boolean { - if (!signingSecret || !payload || !signature?.startsWith('sha256=')) { - return false; - } - - try { - const receivedSignature = signature.substring(7); - const expectedSignature = crypto - .createHmac('sha256', signingSecret) - .update(payload, 'utf8') - .digest('hex'); - - return crypto.timingSafeEqual( - Buffer.from(expectedSignature, 'hex'), - Buffer.from(receivedSignature, 'hex') - ); - } catch (error) { - console.error('Signature verification failed:', error); - return false; - } -} -``` - -### Security Best Practices - -- Store signing secrets in environment variables, never hardcode them -- Always verify signatures before processing webhook data -- Use timing-safe comparison functions (`crypto.timingSafeEqual()` in Node.js, `hmac.compare_digest()` in Python) -- Only accept webhooks over HTTPS -- Log verification failures for security monitoring -- Implement rate limiting on webhook endpoints - -### Troubleshooting - - - -**Cause**: Using parsed JSON instead of raw request body - -**Solution**: Use the raw request body string before any parsing: -- Express: `express.raw({ type: 'application/json' })` and `req.body.toString()` -- Flask: `request.get_data(as_text=True)` -- FastAPI: `await request.body()` then `.decode('utf-8')` - - - -**Cause**: Missing header or incorrect format - -**Solution**: Verify the `X-Webhook-Signature-256` header is present and starts with `sha256=`. Check for header case sensitivity in your framework. - - - -**Cause**: Inconsistent encoding when converting body to string - -**Solution**: Ensure consistent UTF-8 encoding throughout your verification process. - - - ## Support For API support or questions: diff --git a/services/egress-webhook-service/openapi.yaml b/services/egress-webhook-service/openapi.yaml new file mode 100644 index 0000000..21622f0 --- /dev/null +++ b/services/egress-webhook-service/openapi.yaml @@ -0,0 +1,336 @@ +info: + title: Architect - Egress Webhook Events + description: Webhook events delivered by the Egress Webhook Service to subscribed endpoints + version: 1.0.0 + contact: + email: support@tryarchitect.com + name: Architect + url: https://tryarchitect.com +openapi: 3.1.0 +paths: {} +servers: [] +webhooks: + page.version.created: + post: + description: Triggered when a new page version is created. The webhook payload contains details about the newly created page version including page ID, version ID, and creation timestamp. + responses: + "200": + description: Webhook received successfully + summary: Page Version Created + tags: + - Webhooks + requestBody: + content: + application/json: + schema: + type: object + properties: + timestamp: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + tenantId: + type: string + eventId: + type: string + payload: + type: object + properties: + type: + type: string + enum: + - page.version.created + pageId: + type: string + minLength: 1 + versionId: + type: string + minLength: 1 + createdAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + required: + - type + - pageId + - versionId + - createdAt + required: + - timestamp + - tenantId + - eventId + - payload + description: Triggered when a new page version is created. The webhook payload contains details about the newly created page version including page ID, version ID, and creation timestamp. + required: true + page.version.processed: + post: + description: Triggered when a page version is processed. The webhook payload contains details about the processed page version including page ID, version ID, and processing timestamp. + responses: + "200": + description: Webhook received successfully + summary: Page Version Processed + tags: + - Webhooks + requestBody: + content: + application/json: + schema: + type: object + properties: + timestamp: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + tenantId: + type: string + eventId: + type: string + payload: + type: object + properties: + type: + type: string + enum: + - page.version.processed + pageId: + type: string + minLength: 1 + versionId: + type: string + minLength: 1 + processedAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + required: + - type + - pageId + - versionId + - processedAt + required: + - timestamp + - tenantId + - eventId + - payload + description: Triggered when a page version is processed. The webhook payload contains details about the processed page version including page ID, version ID, and processing timestamp. + required: true + page.version.published: + post: + description: Triggered when a page version is published. The webhook payload contains details about the published page version including page ID, version ID, and publishing timestamp. + responses: + "200": + description: Webhook received successfully + summary: Page Version Published + tags: + - Webhooks + requestBody: + content: + application/json: + schema: + type: object + properties: + timestamp: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + tenantId: + type: string + eventId: + type: string + payload: + type: object + properties: + type: + type: string + enum: + - page.version.published + pageId: + type: string + minLength: 1 + versionId: + type: string + minLength: 1 + publishedAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + required: + - type + - pageId + - versionId + - publishedAt + required: + - timestamp + - tenantId + - eventId + - payload + description: Triggered when a page version is published. The webhook payload contains details about the published page version including page ID, version ID, and publishing timestamp. + required: true + form.submitted: + post: + description: Triggered when a form is submitted. The webhook payload contains details about the submitted form including form ID, submission ID, and submission timestamp. + responses: + "200": + description: Webhook received successfully + summary: Form Submitted + tags: + - Webhooks + requestBody: + content: + application/json: + schema: + type: object + properties: + timestamp: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + tenantId: + type: string + eventId: + type: string + payload: + type: object + properties: + type: + type: string + enum: + - form.submitted + formId: + type: string + minLength: 1 + submissionId: + type: string + minLength: 1 + submittedAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + input: + type: object + additionalProperties: {} + required: + - type + - formId + - submissionId + - submittedAt + - input + required: + - timestamp + - tenantId + - eventId + - payload + description: Triggered when a form is submitted. The webhook payload contains details about the submitted form including form ID, submission ID, and submission timestamp. + required: true + form.enriched: + post: + description: Triggered when a form submission is enriched with additional data. The webhook payload contains details about the enriched form including form ID, submission ID, and enriched data. + responses: + "200": + description: Webhook received successfully + summary: Form Enriched + tags: + - Webhooks + requestBody: + content: + application/json: + schema: + type: object + properties: + timestamp: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + tenantId: + type: string + eventId: + type: string + payload: + type: object + properties: + type: + type: string + enum: + - form.enriched + formId: + type: string + minLength: 1 + submissionId: + type: string + minLength: 1 + enrichedAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + enrichedData: + type: object + additionalProperties: {} + required: + - type + - formId + - submissionId + - enrichedAt + - enrichedData + required: + - timestamp + - tenantId + - eventId + - payload + description: Triggered when a form submission is enriched with additional data. The webhook payload contains details about the enriched form including form ID, submission ID, and enriched data. + required: true + form.enrichment_failed: + post: + description: Triggered when form enrichment fails. The webhook payload contains details about the failed enrichment including form ID, submission ID, failure timestamp, and error information. + responses: + "200": + description: Webhook received successfully + summary: Form Enrichment Failed + tags: + - Webhooks + requestBody: + content: + application/json: + schema: + type: object + properties: + timestamp: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + tenantId: + type: string + eventId: + type: string + payload: + type: object + properties: + type: + type: string + enum: + - form.enrichment_failed + formId: + type: string + minLength: 1 + submissionId: + type: string + minLength: 1 + failedAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + error: + type: string + minLength: 1 + required: + - type + - formId + - submissionId + - failedAt + - error + required: + - timestamp + - tenantId + - eventId + - payload + description: Triggered when form enrichment fails. The webhook payload contains details about the failed enrichment including form ID, submission ID, failure timestamp, and error information. + required: true diff --git a/services/external-actor-gateway-service/openapi.yaml b/services/external-actor-gateway-service/openapi.yaml index a538c36..749d544 100644 --- a/services/external-actor-gateway-service/openapi.yaml +++ b/services/external-actor-gateway-service/openapi.yaml @@ -2168,301 +2168,6 @@ paths: - Webhook Subscriptions parameters: - $ref: "#/components/parameters/SubscriptionIdPath" -webhooks: - pageVersionCreated: - post: - description: Triggered when a new page version is created. The webhook payload contains details about the newly created page version including page ID, version ID, and creation timestamp. - responses: - "200": - description: Webhook received successfully - summary: Page Version Created - tags: - - Webhooks - requestBody: - content: - application/json: - schema: - type: object - properties: - eventId: - description: Unique identifier for this event - examples: - - evt_01j5k9m7n8p9q2r3s4t5v6w7x8 - type: string - tenantId: - description: Tenant identifier for the event - examples: - - tenant_01j5k9m7n8p9q2r3s4t5v6w7x8 - type: string - timestamp: - description: Timestamp when the event was processed - examples: - - 2024-01-15T10:30:00Z - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ - payload: - type: object - properties: - type: - description: Event type identifier - type: string - enum: - - page.version.created - pageId: - description: Unique identifier for the page - examples: - - page_01j5k9m7n8p9q2r3s4t5v6w7x8 - type: string - versionId: - description: Unique identifier for the page version that was created - examples: - - pageversion_01j5k9m7n8p9q2r3s4t5v6w7x8 - type: string - pattern: ^pageversion_[0-9a-z]{26}$ - createdAt: - description: Timestamp when the page version was created - examples: - - 2024-01-15T10:30:00Z - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ - required: - - type - - pageId - - versionId - - createdAt - additionalProperties: false - required: - - eventId - - tenantId - - timestamp - - payload - additionalProperties: false - description: Triggered when a new page version is created. The webhook payload contains details about the newly created page version including page ID, version ID, and creation timestamp. - required: true - pageVersionProcessed: - post: - description: Triggered when a page version is processed. The webhook payload contains details about the processed page version including page ID, version ID, and processing timestamp. - responses: - "200": - description: Webhook received successfully - summary: Page Version Processed - tags: - - Webhooks - requestBody: - content: - application/json: - schema: - type: object - properties: - eventId: - description: Unique identifier for this event - examples: - - evt_01j5k9m7n8p9q2r3s4t5v6w7x8 - type: string - tenantId: - description: Tenant identifier for the event - examples: - - tenant_01j5k9m7n8p9q2r3s4t5v6w7x8 - type: string - timestamp: - description: Timestamp when the event was processed - examples: - - 2024-01-15T10:30:00Z - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ - payload: - type: object - properties: - type: - description: Event type identifier - type: string - enum: - - page.version.processed - pageId: - description: Unique identifier for the page - examples: - - page_01j5k9m7n8p9q2r3s4t5v6w7x8 - type: string - versionId: - description: Unique identifier for the page version that was processed - examples: - - pageversion_01j5k9m7n8p9q2r3s4t5v6w7x8 - type: string - processedAt: - description: Timestamp when the page version was processed - examples: - - 2024-01-15T10:30:00Z - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ - required: - - type - - pageId - - versionId - - processedAt - additionalProperties: false - required: - - eventId - - tenantId - - timestamp - - payload - additionalProperties: false - description: Triggered when a page version is processed. The webhook payload contains details about the processed page version including page ID, version ID, and processing timestamp. - required: true - pageVersionPublished: - post: - description: Triggered when a page version is published. The webhook payload contains details about the published page version including page ID, version ID, and publishing timestamp. - responses: - "200": - description: Webhook received successfully - summary: Page Version Published - tags: - - Webhooks - requestBody: - content: - application/json: - schema: - type: object - properties: - eventId: - description: Unique identifier for this event - examples: - - evt_01j5k9m7n8p9q2r3s4t5v6w7x8 - type: string - tenantId: - description: Tenant identifier for the event - examples: - - tenant_01j5k9m7n8p9q2r3s4t5v6w7x8 - type: string - timestamp: - description: Timestamp when the event was processed - examples: - - 2024-01-15T10:30:00Z - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ - payload: - type: object - properties: - type: - description: Event type identifier - type: string - enum: - - page.version.published - pageId: - description: Unique identifier for the page - examples: - - page_01j5k9m7n8p9q2r3s4t5v6w7x8 - type: string - versionId: - description: Unique identifier for the page version that was published - examples: - - pageversion_01j5k9m7n8p9q2r3s4t5v6w7x8 - type: string - pattern: ^pageversion_[0-9a-z]{26}$ - publishedAt: - description: Timestamp when the page version was published - examples: - - 2024-01-15T10:30:00Z - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ - required: - - type - - pageId - - versionId - - publishedAt - additionalProperties: false - required: - - eventId - - tenantId - - timestamp - - payload - additionalProperties: false - description: Triggered when a page version is published. The webhook payload contains details about the published page version including page ID, version ID, and publishing timestamp. - required: true - formSubmitted: - post: - description: Triggered when a form is submitted. The webhook payload contains details about the submitted form including form ID, submission ID, and submission timestamp. - responses: - "200": - description: Webhook received successfully - summary: Form Submitted - tags: - - Webhooks - requestBody: - content: - application/json: - schema: - type: object - properties: - eventId: - description: Unique identifier for this event - examples: - - evt_01j5k9m7n8p9q2r3s4t5v6w7x8 - type: string - tenantId: - description: Tenant identifier for the event - examples: - - tenant_01j5k9m7n8p9q2r3s4t5v6w7x8 - type: string - timestamp: - description: Timestamp when the event was processed - examples: - - 2024-01-15T10:30:00Z - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ - payload: - type: object - properties: - type: - description: Event type identifier - type: string - enum: - - form.submitted - formId: - description: Unique identifier for the form - examples: - - form_01j5k9m7n8p9q2r3s4t5v6w7x8 - type: string - submissionId: - description: Unique identifier for the form submission - examples: - - formsubmission_01j5k9m7n8p9q2r3s4t5v6w7x8 - type: string - submittedAt: - description: Timestamp when the form submission was submitted - examples: - - 2024-01-15T10:30:00Z - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ - input: - description: Input data for the form submission - examples: - - name: email - value: test@example.com - type: object - additionalProperties: {} - required: - - type - - formId - - submissionId - - submittedAt - - input - additionalProperties: false - required: - - eventId - - tenantId - - timestamp - - payload - additionalProperties: false - description: Triggered when a form is submitted. The webhook payload contains details about the submitted form including form ID, submission ID, and submission timestamp. - required: true tags: - name: Page Groups description: Page group management operations diff --git a/webhooks.mdx b/webhooks.mdx new file mode 100644 index 0000000..8f33e59 --- /dev/null +++ b/webhooks.mdx @@ -0,0 +1,87 @@ +--- +title: "Webhook Security" +description: "Learn how to verify webhook signatures and implement secure webhook handling" +--- + +## Webhook Signature Verification + +Webhook requests include an HMAC-SHA256 signature in the `X-Webhook-Signature-256` header that you should verify to ensure authenticity and prevent tampering. + +### Verification Process + +1. Extract the signature from the `X-Webhook-Signature-256` header (format: `sha256=`) +2. Get the raw request body before parsing +3. Calculate the expected signature using your webhook signing secret +4. Compare signatures using a timing-safe comparison function +5. Process the webhook only if signatures match + + +Always use the raw request body for verification, not the parsed JSON. Use timing-safe comparison functions to prevent timing attacks. + + +### Implementation Example + +```typescript +import crypto from 'node:crypto'; +import express from 'express'; + +function verifyWebhookSignature( + signingSecret: string, + payload: string, + signature: string +): boolean { + if (!signingSecret || !payload || !signature?.startsWith('sha256=')) { + return false; + } + + try { + const receivedSignature = signature.substring(7); + const expectedSignature = crypto + .createHmac('sha256', signingSecret) + .update(payload, 'utf8') + .digest('hex'); + + return crypto.timingSafeEqual( + Buffer.from(expectedSignature, 'hex'), + Buffer.from(receivedSignature, 'hex') + ); + } catch (error) { + console.error('Signature verification failed:', error); + return false; + } +} +``` + +### Security Best Practices + +- Store signing secrets in environment variables, never hardcode them +- Always verify signatures before processing webhook data +- Use timing-safe comparison functions (`crypto.timingSafeEqual()` in Node.js, `hmac.compare_digest()` in Python) +- Only accept webhooks over HTTPS +- Log verification failures for security monitoring +- Implement rate limiting on webhook endpoints + +### Troubleshooting + + + +**Cause**: Using parsed JSON instead of raw request body + +**Solution**: Use the raw request body string before any parsing: +- Express: `express.raw({ type: 'application/json' })` and `req.body.toString()` +- Flask: `request.get_data(as_text=True)` +- FastAPI: `await request.body()` then `.decode('utf-8')` + + + +**Cause**: Missing header or incorrect format + +**Solution**: Verify the `X-Webhook-Signature-256` header is present and starts with `sha256=`. Check for header case sensitivity in your framework. + + + +**Cause**: Inconsistent encoding when converting body to string + +**Solution**: Ensure consistent UTF-8 encoding throughout your verification process. + + \ No newline at end of file