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.

Base URL:https://apisign.io/api

Webhook Endpoints


Available Events

EventDescription
contract.createdA new contract was created
contract.sentA contract was sent to signers
contract.viewedA signer viewed the contract
contract.signedA signer completed their signature
contract.completedAll signers have signed
contract.cancelledA contract was cancelled
contract.expiredA contract expired
template.createdA new template was created
template.updatedA template was modified
template.deletedA 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:

HeaderDescription
X-APISign-EventThe event type that triggered the webhook
X-APISign-SignatureHMAC signature for verification
X-APISign-TimestampUnix timestamp when event occurred
X-APISign-Webhook-IDUnique 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:

AttemptDelay
InitialImmediate
Retry 11 minute
Retry 25 minutes
Retry 315 minutes
Retry 41 hour
Retry 56 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.