Skip to main content
Firma sends webhook events to notify your application about signing lifecycle changes, document completions, and workspace activities. Webhooks enable real-time integrations without polling.

Common use cases

  • Send internal notifications when documents are signed
  • Update your database when signing requests are completed
  • Trigger downstream workflows (invoicing, provisioning, etc.)
  • Track signing request status changes in real-time

Event types

Firma sends the following event types:

Signing Request Events

  • signing_request.created - New signing request created
  • signing_request.sent - Signing request sent to recipients
  • signing_request.viewed - Recipient viewed the document
  • signing_request.completed - All recipients finished signing
  • signing_request.expired - Signing request expired
  • signing_request.cancelled - Signing request was cancelled
  • signing_request.updated - Signing request metadata updated
  • signing_request.deleted - Signing request deleted (before sending)
  • signing_request.certificate.generated - Signing certificate generated
  • signing_request.reminder.sent - Reminder sent to recipients

Recipient Events

  • signing_request.recipient.signed - Recipient completed signing
  • signing_request.recipient.declined - Recipient declined to sign
  • signing_request.recipient.identity_changed - Recipient changed their identity (name, company, etc.) during signing

Template Events

  • template.updated - Template modified
  • template.used - Template used to create a signing request

Organization Seal Events

  • organization_seal.created - New seal created
  • organization_seal.updated - Seal image replaced (new version), renamed, or default flag changed
  • organization_seal.deleted - Seal revoked
  • organization_seal.erased - Revoked seal lineage permanently erased

Signing Request Seal Events

  • signing_request.seal.applied - Seal applied to a document
  • signing_request.seal.paused - Seal participant paused (revoked seal with stop pending, or integrity mismatch)
  • signing_request.seal.swapped - Paused seal participant swapped to a different seal

Workspace Events

  • workspace.created - New workspace created
  • workspace.updated - Workspace modified

Domain Events

  • domain.verified - Domain verified successfully
  • domain.verification.failed - Domain verification failed

Two levels of enablement

Webhook delivery requires the webhook’s own switch and the master switch for its scope. Company webhooks use the company master switch. Workspace webhooks use that workspace’s master switch and do not require the company switch. At least one scope (company or workspace) must have its master switch on for any webhook under that scope to deliver. Turning on the workspace master switch also generates a signing secret for that workspace if one doesn’t already exist.
POST /webhooks/{id}/test bypasses the master switch. A test delivery can succeed even while the master switch is off and live events are not being delivered - a passing test is not confirmation that real events will fire.
When the master switch is off, Firma skips the event before logging a delivery attempt. consecutive_failures stays at 0 and the webhook shows no failure history, even though nothing is being delivered - there’s no error to alert you.

Permissions

Toggling the company-level master switch requires owner or admin access to the company; view-only members cannot enable or disable it. Attempting to toggle it without sufficient permissions returns a permission error.

Creating a webhook

Create webhooks via the API or dashboard:
Your webhook URL must use HTTPS and respond within 5 seconds. Use POST /webhooks/{id}/test after creation to verify your endpoint is receiving events correctly.

Webhook payload structure

All webhook events follow this standard structure:
Test deliveries from POST /webhooks/{id}/test use a different shape: type (webhook.test), webhook_id, timestamp and data. They have no id, so skip your idempotency check for them.

Security: Signature verification (Required)

Always verify webhook signatures to prevent spoofing attacks. Do not process webhooks without signature verification.
Firma signs all webhook requests using HMAC SHA-256. Your webhook endpoint receives these headers:
  • X-Firma-Signature - HMAC signature using current signing secret
  • X-Firma-Signature-Old - HMAC signature using previous secret (during 7-day rotation grace period)
  • X-Firma-Event - Event type (e.g., signing_request.completed)
  • X-Firma-Delivery - ID of this event’s delivery to your endpoint. It stays the same on every retry, automatic or manual. Use the payload id to identify the event.

Signature format

Firma uses a timestamped signature header:
  • Header: X-Firma-Signature: t=1707500000,v1=abc123def456...
  • t = Unix timestamp (seconds) when the signature was generated
  • v1 = HMAC-SHA256 hex digest
  • Signed payload format: {timestamp}.{json_body}
  • The HMAC key is your signing secret exactly as shown in the dashboard (64 characters). Don’t hex-decode it.
  • X-Firma-Signature and X-Firma-Signature-Old each carry their own t, so read the timestamp from the header you are verifying.
Example:

Get your signing secret

  1. Navigate to your Firma dashboard
  2. View webhook details to retrieve the signing secret
  3. Store the secret securely (environment variable or secrets manager)

Verification example - Node.js (Express)

Verification example - Python (Flask)

Handling secret rotation

Rotate your webhook signing secret from the dashboard, or with POST /webhooks/rotate-secret for company webhooks or POST /workspaces/{id}/webhooks/rotate-secret for workspace webhooks. Through the API, you can rotate a secret once per hour. Workspace webhooks are signed with that workspace’s own secret, not the company secret.
  1. Firma generates a new secret
  2. For 7 days, Firma sends both signatures:
  • X-Firma-Signature (new secret)
  • X-Firma-Signature-Old (previous secret)
  1. After 7 days, only X-Firma-Signature is sent. GET /webhooks/secret-status or GET /workspaces/{id}/webhooks/secret-status returns grace_period_ends_at.
Firma keeps only one previous secret. If you rotate again within the 7 days, the previous secret is replaced, and an endpoint that still holds the secret from before the first rotation stops verifying deliveries at once. Deploy each new secret before you rotate again.
Implementation: Verify X-Firma-Signature with the secret your endpoint is configured with. If that fails, verify X-Firma-Signature-Old with the same secret. Until you deploy the new secret, your configured secret is the previous one, which is the one X-Firma-Signature-Old is signed with. After you deploy it, X-Firma-Signature matches. Deploy the new secret within the 7 days, and configure one secret at a time: an endpoint that accepts both secrets keeps trusting the old one.

If a secret may be compromised

  1. Rotate the secret immediately.
  2. Deploy the new secret to your endpoint straight away.
From then on, a request signed with the old secret fails both checks at your endpoint, even while Firma still sends X-Firma-Signature-Old, so the 7-day overlap does not extend trust in the old secret.

Retry behavior

Firma does not follow HTTP redirects when delivering webhooks. Register the exact URL your server serves, including its scheme, hostname, path, and trailing-slash form. Firma automatically retries failed webhook deliveries:
  • Attempts: Up to 3 per event: immediately, then at least 5 minutes after the first failure, then at least 1 hour after the second. Retries run on a 5-minute cycle, so expect roughly 5 to 10 minutes and 60 to 65 minutes, or later when many retries are queued.
  • Timeout: Your endpoint must respond within 5 seconds
  • Success: Any 2xx status code indicates success. Redirects count as failures.
  • Same event, new signature: Every attempt carries the same X-Firma-Delivery and, for real events, the same payload id, and is signed again with a fresh timestamp. The JSON key order of a retried body can differ from the first attempt, so always verify the raw body exactly as received.
  • Auto-disable: When consecutive_failures reaches 50, the webhook is disabled automatically and Firma emails the account owner. Re-enable it with PUT /webhooks/{id} ("enabled": true) or in the dashboard.
  • After the last attempt: An event that fails all 3 attempts is not retried automatically. An administrator can retry it from the webhook event log in the dashboard.
  • Not retried: Test deliveries from POST /webhooks/{id}/test are sent once. A delivery to a URL that fails Firma’s delivery-time address check (for example, a URL that resolves to a private or loopback address) fails without a retry.
Best practice: Respond with 200 immediately, then process events asynchronously (queue, background job, etc.) to avoid timeouts.

Delivery limits

Webhooks are notifications, not a complete event log:
  • Events are not delivered in a guaranteed order, and a retried event can arrive after a later one. Use the payload created_at as a hint and fetch the current state from the API.
  • Events raised while a webhook or its master switch is off are not stored and are not delivered later. Re-enabling a webhook does not replay them.
  • When a webhook already has 100 events from the last 60 seconds, new events for it are skipped and never delivered.
  • There is no API to list or replay events. To reconcile after an outage, list signing requests with GET /signing-requests (for example status=finished&sort_by=finished_on&sort_order=desc) and fetch each one with GET /signing-requests/{id}.

Idempotency

Always handle duplicate events using the event id:

Monitoring webhook health

Monitor your webhook’s health by checking the webhook details:
Response includes:
  • consecutive_failures - Failure counter used for automatic disabling. It resets to 0 after a successful delivery.
  • enabled - Whether the webhook is active. It is disabled automatically when consecutive_failures reaches 50.
  • auto_disabled_at - When the webhook was disabled automatically, otherwise null
Rate limit: Each /webhooks endpoint allows 60 requests per minute per API key. POST /webhooks/{id}/test allows 10 requests per minute.

Troubleshooting

Common issues

Webhooks configured but events don’t fire This is the most common cause of “my webhook isn’t working” reports. Per-webhook settings can be entirely correct while the account-level master switch is off, silently skipping every event.
  • Confirm the master switch is on for the right scope: company-level webhooks need the company’s switch on, workspace-level webhooks need that workspace’s switch on.
  • Don’t trust a passing test as proof - POST /webhooks/{id}/test bypasses the master switch, so it can succeed while live events don’t fire.
  • Check consecutive_failures on the webhook - if it reads 0 and no events show up, that also points to the master switch, since skipped events are never logged as failed delivery attempts.
  • If you can’t find or toggle the master switch, you may not have owner or admin permissions on the company; ask an owner or admin to enable it from Settings > Webhooks.
401 Unauthorized / Invalid signature
  • Verify you’re using the correct signing secret
  • Check that you’re hashing the raw request body (not parsed JSON)
  • Ensure you’re using HMAC SHA-256, not other hash algorithms
Timeouts / 504 errors
  • Respond with 200 immediately, process asynchronously
  • Check your endpoint responds within 5 seconds
  • Use background jobs/queues for heavy processing
Duplicate events
  • Implement idempotency using the event id
  • Store processed event IDs in your database
Webhook auto-disabled
  • Check consecutive_failures and recent events in the webhook event log in the dashboard
  • Fix endpoint issues, then re-enable webhook via API or dashboard

Testing webhooks locally

Use a tunnel service like ngrok for local development:

Production checklist

  • Verify HMAC signatures on all webhook requests
  • Handle signature rotation (check both X-Firma-Signature and X-Firma-Signature-Old)
  • Respond with 200 within 5 seconds
  • Process events asynchronously (queues/background jobs)
  • Implement idempotency using the event id
  • Store signing secret securely (env var or secrets manager)
  • Monitor consecutive_failures metric via GET webhook endpoint
  • Set up alerts for webhook failures
  • Log all webhook events for debugging
  • Test with all subscribed event types
  • Stay within rate limits (See Rate Limit Guide)

Next steps