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.signed - Recipient completed signing
signing_request.completed - All recipients finished signing
signing_request.cancelled - Signing request was cancelled
signing_request.expired - Signing request expired
signing_request.updated - Signing request metadata updated
Template Events
template.created - New template created
template.updated - Template modified
template.deleted - Template deleted
Workspace Events
workspace.created - New workspace created
workspace.updated - Workspace modified
Creating a webhook
Create webhooks via the API or dashboard:
curl -X POST "https://api.firma.dev/functions/v1/signing-request-api/webhooks" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/firma",
"events": [
"signing_request.completed",
"signing_request.signed"
],
"description": "Production webhook for signing events"
}'
Your webhook URL must use HTTPS and respond within 5 seconds. Firma will send a test event during creation to verify the endpoint.
Webhook payload structure
All webhook events follow this standard structure:
{
"event_id": "evt_a1b2c3d4",
"event_type": "signing_request.completed",
"timestamp": "2025-10-03T14:30:00Z",
"company_id": "comp_123",
"workspace_id": "ws_456",
"data": {
"signing_request_id": "sr_789",
"template_id": "tmpl_012",
"status": "completed",
"finished_date": "2025-10-03T14:29:55Z",
"recipients": [
{
"id": "rec_abc",
"first_name": "Alice",
"last_name": "Johnson",
"email": "alice@example.com",
"finished_date": "2025-10-03T14:29:55Z"
}
]
}
}
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 24-hour rotation grace period)
X-Firma-Event - Event type (e.g., signing_request.completed)
X-Firma-Delivery - Unique delivery attempt ID
Get your signing secret
- Navigate to your Firma dashboard
- View webhook details to retrieve the signing secret
- Store the secret securely (environment variable or secrets manager)
Verification example — Node.js (Express)
import express from 'express'
import crypto from 'crypto'
const app = express()
// Use raw body for signature verification
app.post('/webhooks/firma',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-firma-signature']
const signatureOld = req.headers['x-firma-signature-old']
const payload = req.body.toString('utf8')
const signingSecret = process.env.FIRMA_WEBHOOK_SECRET
// Verify signature
if (!verifySignature(payload, signature, signingSecret)) {
// During secret rotation, also check old signature
if (!signatureOld || !verifySignature(payload, signatureOld, signingSecret)) {
console.error('Invalid webhook signature')
return res.status(401).json({ error: 'Invalid signature' })
}
}
// Parse and process event
const event = JSON.parse(payload)
processWebhookEvent(event)
// Respond immediately
res.status(200).json({ received: true })
}
)
function verifySignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
)
}
async function processWebhookEvent(event) {
// Process asynchronously - don't block the response
switch (event.event_type) {
case 'signing_request.completed':
await handleSigningCompleted(event.data)
break
case 'signing_request.signed':
await handleRecipientSigned(event.data)
break
// ... handle other event types
}
}
app.listen(3000)
Verification example — Python (Flask)
from flask import Flask, request, jsonify
import hmac
import hashlib
import os
import json
app = Flask(__name__)
@app.route('/webhooks/firma', methods=['POST'])
def firma_webhook():
signature = request.headers.get('X-Firma-Signature')
signature_old = request.headers.get('X-Firma-Signature-Old')
payload = request.get_data()
signing_secret = os.environ['FIRMA_WEBHOOK_SECRET']
# Verify signature
if not verify_signature(payload, signature, signing_secret):
# During secret rotation, also check old signature
if not signature_old or not verify_signature(payload, signature_old, signing_secret):
return jsonify({'error': 'Invalid signature'}), 401
# Parse and process event
event = json.loads(payload)
process_webhook_event(event)
# Respond immediately
return jsonify({'received': True}), 200
def verify_signature(payload, signature, secret):
expected_signature = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
def process_webhook_event(event):
# Process asynchronously - don't block the response
event_type = event['event_type']
if event_type == 'signing_request.completed':
handle_signing_completed(event['data'])
elif event_type == 'signing_request.signed':
handle_recipient_signed(event['data'])
# ... handle other event types
if __name__ == '__main__':
app.run(port=3000)
Handling secret rotation
When you rotate your webhook signing secret:
- Firma generates a new secret
- For 24 hours, Firma sends both signatures:
X-Firma-Signature (new secret)
X-Firma-Signature-Old (previous secret)
- After 24 hours, only
X-Firma-Signature is sent
Implementation: Check X-Firma-Signature first. If verification fails and X-Firma-Signature-Old exists, verify against the old secret.
Retry behavior
Firma automatically retries failed webhook deliveries:
- Retry schedule: 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours
- Total attempts: Up to 5 retries per event
- Timeout: Your endpoint must respond within 5 seconds
- Success: Any 2xx status code indicates success
- Auto-disable: After 50 consecutive failures, the webhook is automatically disabled
Best practice: Respond with 200 immediately, then process events asynchronously (queue, background job, etc.) to avoid timeouts.
Idempotency
Always handle duplicate events using the event_id:
async function processWebhookEvent(event) {
// Check if already processed
const exists = await db.webhookEvents.findOne({ event_id: event.event_id })
if (exists) {
console.log(`Event ${event.event_id} already processed`)
return
}
// Store event_id to prevent duplicates
await db.webhookEvents.create({ event_id: event.event_id, processed_at: new Date() })
// Process event
// ...
}
Monitoring webhook health
Monitor your webhook’s health by checking the webhook details:
## Get webhook details
curl "https://api.firma.dev/functions/v1/signing-request-api/webhooks/{id}" \
-H "Authorization: Bearer YOUR_API_KEY"
Response includes:
consecutive_failures - Number of consecutive failed deliveries
last_failure_at - Timestamp of most recent failure
enabled - Whether webhook is active (auto-disabled after 50 failures)
last_success_at - Timestamp of most recent successful delivery
Rate limit: GET webhook requests support 60 requests per minute per API key.
Troubleshooting
Common issues
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
event_id
- Store processed event IDs in your database
Webhook auto-disabled
- Check
consecutive_failures and recent event logs
- Fix endpoint issues, then re-enable webhook via API or dashboard
Testing webhooks locally
Use a tunnel service like ngrok for local development:
# Start ngrok
ngrok http 3000
# Use the HTTPS URL in your webhook configuration
# https://abc123.ngrok.io/webhooks/firma
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
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