Skip to main content

API Webhooks

SDD Classification: L3-Technical Authority: Engineering Team Review Cycle: Quarterly
Webhooks enable your application to receive real-time notifications when events occur in Materi. This guide covers webhook setup, event types, security, and best practices.

Webhook Overview


Setting Up Webhooks

Create Webhook Endpoint

POST /api/v1/webhooks
Authorization: Bearer <token>
Content-Type: application/json

{
  "url": "https://yourapp.com/webhooks/materi",
  "events": ["document.created", "document.updated"],
  "secret": "your_webhook_secret_key",
  "description": "Production webhook for document events"
}
Response:
{
  "id": "wh_abc123",
  "url": "https://yourapp.com/webhooks/materi",
  "events": ["document.created", "document.updated"],
  "status": "active",
  "created_at": "2025-01-07T10:00:00Z",
  "secret_preview": "your_...key"
}

Webhook Properties

PropertyTypeRequiredDescription
urlstringYesHTTPS endpoint URL
eventsarrayYesEvent types to subscribe
secretstringYesSigning secret (32+ chars)
descriptionstringNoHuman-readable description
workspace_idstringNoScope to specific workspace

Event Types

Document Events

EventDescriptionPayload
document.createdDocument createdDocument object
document.updatedDocument content changedDocument diff
document.deletedDocument deletedDocument ID
document.sharedDocument shared with userShare details
document.version.createdNew version savedVersion object

Workspace Events

EventDescriptionPayload
workspace.createdWorkspace createdWorkspace object
workspace.updatedWorkspace settings changedUpdated fields
workspace.member.addedUser joined workspaceMember details
workspace.member.removedUser left/removedMember details

Collaboration Events

EventDescriptionPayload
collaboration.startedUser started editingSession info
collaboration.endedUser stopped editingSession summary
comment.createdComment addedComment object
comment.resolvedComment resolvedComment ID

AI Events

EventDescriptionPayload
ai.generation.completedAI generation finishedGeneration result
ai.generation.failedAI generation failedError details

Webhook Payload Format

Standard Payload Structure

{
  "id": "evt_abc123def456",
  "type": "document.created",
  "created_at": "2025-01-07T10:30:00Z",
  "data": {
    "id": "doc_xyz789",
    "title": "New Document",
    "workspace_id": "ws_123",
    "owner_id": "user_456",
    "created_at": "2025-01-07T10:30:00Z"
  },
  "metadata": {
    "workspace_id": "ws_123",
    "triggered_by": "user_456",
    "ip_address": "203.0.113.42"
  }
}

Payload Properties

PropertyTypeDescription
idstringUnique event ID
typestringEvent type
created_atdatetimeEvent timestamp
dataobjectEvent-specific data
metadataobjectAdditional context

Webhook Security

Signature Verification

Every webhook includes a signature header for verification:
X-Materi-Signature: sha256=abc123def456...
X-Materi-Timestamp: 1704625800

Verifying Signatures

const crypto = require('crypto');

function verifyWebhook(payload, signature, timestamp, secret) {
  // Check timestamp (prevent replay attacks)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - timestamp) > 300) {
    throw new Error('Timestamp too old');
  }

  // Compute expected signature
  const signedPayload = `${timestamp}.${payload}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Compare signatures
  const expected = `sha256=${expectedSignature}`;
  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    throw new Error('Invalid signature');
  }

  return true;
}

// Express.js middleware
app.post('/webhooks/materi', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-materi-signature'];
  const timestamp = req.headers['x-materi-timestamp'];

  try {
    verifyWebhook(req.body, signature, timestamp, WEBHOOK_SECRET);

    const event = JSON.parse(req.body);
    handleEvent(event);

    res.status(200).send('OK');
  } catch (error) {
    console.error('Webhook verification failed:', error);
    res.status(400).send('Invalid signature');
  }
});

Python Verification

import hmac
import hashlib
import time

def verify_webhook(payload: bytes, signature: str, timestamp: str, secret: str) -> bool:
    # Check timestamp
    current_time = int(time.time())
    if abs(current_time - int(timestamp)) > 300:
        raise ValueError("Timestamp too old")

    # Compute expected signature
    signed_payload = f"{timestamp}.{payload.decode()}"
    expected_signature = hmac.new(
        secret.encode(),
        signed_payload.encode(),
        hashlib.sha256
    ).hexdigest()

    # Compare signatures
    expected = f"sha256={expected_signature}"
    return hmac.compare_digest(signature, expected)

Delivery & Retries

Retry Policy

Retry Schedule

AttemptDelayTotal Time
1Immediate0
21 minute1 min
35 minutes6 min
430 minutes36 min
52 hours~2.5 hours
624 hours~26.5 hours

Response Requirements

  • Timeout: 30 seconds
  • Success: 2xx status codes
  • Failure: 4xx, 5xx, or timeout

Managing Webhooks

List Webhooks

GET /api/v1/webhooks
Authorization: Bearer <token>

Update Webhook

PATCH /api/v1/webhooks/{webhook_id}
Authorization: Bearer <token>
Content-Type: application/json

{
  "events": ["document.created", "document.updated", "document.deleted"],
  "status": "active"
}

Delete Webhook

DELETE /api/v1/webhooks/{webhook_id}
Authorization: Bearer <token>

View Delivery History

GET /api/v1/webhooks/{webhook_id}/deliveries
Authorization: Bearer <token>
Response:
{
  "data": [
    {
      "id": "del_abc123",
      "event_id": "evt_xyz789",
      "event_type": "document.created",
      "status": "success",
      "response_code": 200,
      "response_time_ms": 145,
      "delivered_at": "2025-01-07T10:30:01Z"
    },
    {
      "id": "del_def456",
      "event_id": "evt_uvw012",
      "event_type": "document.updated",
      "status": "failed",
      "response_code": 500,
      "error": "Internal Server Error",
      "retries": 3,
      "next_retry_at": "2025-01-07T13:00:00Z"
    }
  ]
}

Testing Webhooks

Test Endpoint

Send a test event to your webhook:
POST /api/v1/webhooks/{webhook_id}/test
Authorization: Bearer <token>
Content-Type: application/json

{
  "event_type": "document.created"
}

Local Development

Use tools like ngrok for local testing:
# Start ngrok tunnel
ngrok http 3000

# Register webhook with ngrok URL
POST /api/v1/webhooks
{
  "url": "https://abc123.ngrok.io/webhooks/materi",
  "events": ["document.created"]
}

Best Practices

Endpoint Design

  1. Respond quickly - Return 200 immediately, process async
  2. Idempotency - Handle duplicate deliveries
  3. Logging - Log all webhook receipts
  4. Queuing - Queue events for processing

Example Handler

app.post('/webhooks/materi', async (req, res) => {
  // Verify signature first
  if (!verifySignature(req)) {
    return res.status(400).send('Invalid signature');
  }

  const event = req.body;

  // Acknowledge receipt immediately
  res.status(200).send('OK');

  // Process asynchronously
  try {
    await queueEvent(event);
  } catch (error) {
    console.error('Failed to queue event:', error);
    // Event will be retried
  }
});

async function processEvent(event) {
  // Check for duplicates
  if (await isDuplicate(event.id)) {
    console.log('Duplicate event, skipping:', event.id);
    return;
  }

  // Process based on event type
  switch (event.type) {
    case 'document.created':
      await handleDocumentCreated(event.data);
      break;
    case 'document.updated':
      await handleDocumentUpdated(event.data);
      break;
    // ... other handlers
  }

  // Mark as processed
  await markProcessed(event.id);
}


Document Status: Complete Version: 2.0