Webhooks
Webhooks allow your application to receive real-time notifications when events occur in API Sign. Instead of repeatedly polling our API, webhooks push event data to your application immediately when something happens.
https://apisign.io/apiWebhook Endpoints
Available Events
| Event | Description |
|---|---|
contract.created | A new contract was created |
contract.sent | A contract was sent to signers |
contract.viewed | A signer viewed the contract |
contract.signed | A signer completed their signature |
contract.completed | All signers have signed |
contract.cancelled | A contract was cancelled |
contract.expired | A contract expired |
template.created | A new template was created |
template.updated | A template was modified |
template.deleted | A template was deleted |
Webhook Payload
All webhook payloads follow this structure:
{
"id": "evt_1234567890",
"type": "contract.signed",
"created": 1642253100,
"data": {
"contract": {
"id": "ctr_1234567890",
"title": "Service Agreement",
"status": "partially_signed"
},
"signer": {
"id": "sgn_client_123",
"email": "john@acme.com",
"signed_at": "2024-01-15T14:30:00Z"
}
}
}
Webhook Headers
All webhook requests include these headers:
| Header | Description |
|---|---|
X-APISign-Event | The event type that triggered the webhook |
X-APISign-Signature | HMAC signature for verification |
X-APISign-Timestamp | Unix timestamp when event occurred |
X-APISign-Webhook-ID | Unique identifier for this webhook |
Signature Verification
Webhook Security
Always verify webhook signatures to ensure requests are genuine. API Sign includes a signature in the webhook headers that you should validate.
Node.js Example
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret, timestamp) {
// Check timestamp to prevent replay attacks (within 5 minutes)
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - timestamp) > 300) {
throw new Error('Request timestamp too old');
}
// Create signed payload
const signedPayload = timestamp + '.' + payload;
// Generate expected signature
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload, 'utf8')
.digest('hex');
// Extract signature from header (format: sha256=signature)
const receivedSignature = signature.split('=')[1];
// Compare signatures using time-safe comparison
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
}
// Usage in Express.js
app.post('/webhooks/apisign', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.get('X-APISign-Signature');
const timestamp = req.get('X-APISign-Timestamp');
const payload = req.body.toString();
try {
const isValid = verifyWebhookSignature(
payload,
signature,
process.env.APISIGN_WEBHOOK_SECRET,
parseInt(timestamp)
);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
handleWebhookEvent(event);
res.status(200).send('OK');
} catch (error) {
console.error('Webhook verification failed:', error);
res.status(400).send('Verification failed');
}
});
Python Example
import hmac
import hashlib
import time
def verify_webhook_signature(payload, signature, secret, timestamp):
# Check timestamp (within 5 minutes)
current_time = int(time.time())
if abs(current_time - int(timestamp)) > 300:
raise ValueError('Request timestamp too old')
# Create signed payload
signed_payload = f"{timestamp}.{payload}"
# Generate expected signature
expected_signature = hmac.new(
secret.encode('utf-8'),
signed_payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Extract signature from header
received_signature = signature.split('=')[1]
# Compare signatures
return hmac.compare_digest(expected_signature, received_signature)
Retry Policy
API Sign implements automatic retry logic for failed webhook deliveries:
| Attempt | Delay |
|---|---|
| Initial | Immediate |
| Retry 1 | 1 minute |
| Retry 2 | 5 minutes |
| Retry 3 | 15 minutes |
| Retry 4 | 1 hour |
| Retry 5 | 6 hours |
Failure Conditions
Webhooks are considered failed if:
- HTTP response code is not 2xx
- Request times out (10 seconds)
- Connection cannot be established
- SSL/TLS handshake fails
Best Practices
Respond Quickly
Return a 2xx response immediately, then process the event asynchronously:
app.post('/webhooks/apisign', (req, res) => {
// Verify signature
verifySignature(req);
// Queue for async processing
const event = JSON.parse(req.body);
webhookQueue.add('process-event', event);
// Respond immediately
res.status(200).send('OK');
});
Handle Duplicates
Implement idempotency using event IDs:
const processedEvents = new Set();
function handleWebhookEvent(event) {
if (processedEvents.has(event.id)) {
console.log('Event already processed:', event.id);
return;
}
processEvent(event);
processedEvents.add(event.id);
}
Use HTTPS
Webhook endpoints must use HTTPS in production. HTTP endpoints are not allowed.
Testing Webhooks
Local Development
Use ngrok to expose your local server:
ngrok http 3000
# Use the HTTPS URL: https://abc123.ngrok.io/webhooks/apisign
Test Events
Test events include a test: true property:
{
"id": "evt_test_1234567890",
"type": "contract.signed",
"test": true,
"data": { ... }
}
Test Events
Test events can be sent from the dashboard under Settings → Webhooks → Test Webhook.