> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.deel.wtf/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.deel.wtf/_mcp/server.

# Quickstart

> Set up your first Deel webhook in minutes

> **Info**
>
> **Not a developer?** You can set up webhooks without writing any code using the [Deel Developer Center](/api/webhooks/no-code). This guide is for developers who want to integrate webhooks programmatically.

## What You'll Learn

This quickstart guide will walk you through:

1. Creating a webhook endpoint to receive events
2. Verifying webhook signatures for security
3. Subscribing to specific events
4. Testing your webhook integration

> **Note**
>
> **Time to complete:** \~15 minutes

## Prerequisites

Before you begin, ensure you have:

* **API Token** – Get your API token from the Deel Developer Center
* **Development Environment** – Node.js, Python, Go, or PHP installed locally
* **Testing Tool** – ngrok or a similar tool to expose localhost

## Step 1: Create a Webhook Endpoint

First, create a simple HTTP endpoint that can receive POST requests from Deel.

> **Tip**
>
> **Choose your language:** We'll show examples in multiple languages. Pick the one you're most comfortable with!

**`Node.js`**

```javascript Node.js
// webhook-server.js
const express = require('express');
const crypto = require('crypto');

const app = express();

// Middleware to capture raw body for signature verification
app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString();
  }
}));

// Webhook endpoint
app.post('/webhooks/deel', (req, res) => {
  console.log('✅ Webhook received!');
  console.log('Event type:', req.body.data?.meta?.event_type);

  // Always respond with 200 immediately
  res.status(200).send('OK');
});

app.listen(3000, () => {
  console.log('🚀 Webhook server running on http://localhost:3000');
});
```

**`Python`**

```python Python
# webhook_server.py
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhooks/deel', methods=['POST'])
def handle_webhook():
    print('✅ Webhook received!')

    event = request.json
    print(f"Event type: {event['data']['meta']['event_type']}")

    # Always respond with 200 immediately
    return jsonify({'status': 'success'}), 200

if __name__ == '__main__':
    app.run(port=3000, debug=True)
```

**`Go`**

```go Go
// webhook_server.go
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
)

type WebhookPayload struct {
    Data struct {
        Meta struct {
            EventType string `json:"event_type"`
        } `json:"meta"`
    } `json:"data"`
}

func handleWebhook(w http.ResponseWriter, r *http.Request) {
    fmt.Println("✅ Webhook received!")

    var payload WebhookPayload
    json.NewDecoder(r.Body).Decode(&payload)

    fmt.Printf("Event type: %s\n", payload.Data.Meta.EventType)

    // Always respond with 200 immediately
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("OK"))
}

func main() {
    http.HandleFunc("/webhooks/deel", handleWebhook)
    fmt.Println("🚀 Webhook server running on http://localhost:3000")
    log.Fatal(http.ListenAndServe(":3000", nil))
}
```

**`PHP`**

```php PHP
<?php
// webhook_server.php

// Webhook endpoint
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    echo "✅ Webhook received!\n";

    $payload = json_decode(file_get_contents('php://input'), true);
    $eventType = $payload['data']['meta']['event_type'] ?? 'unknown';

    echo "Event type: $eventType\n";

    // Always respond with 200 immediately
    http_response_code(200);
    echo "OK";
}
?>
```

> **Note**
>
> **Start your server** and make sure it's running on port 3000. We'll expose it publicly in the next step.

## Step 2: Expose Your Local Server

Use ngrok to create a public HTTPS URL for your local server:

#### Install ngrok

Download and install ngrok from [ngrok.com](https://ngrok.com)

```bash
# Or install via homebrew (macOS)
brew install ngrok
```

#### Start ngrok tunnel

Run ngrok to expose port 3000:

```bash
ngrok http 3000
```

You'll see output like:

```
Forwarding  https://abc123.ngrok.io -> http://localhost:3000
```

#### Copy your HTTPS URL

Copy the `https://` URL (e.g., `https://abc123.ngrok.io`)

Your webhook URL will be: `https://abc123.ngrok.io/webhooks/deel`

> **Warning**
>
> **Keep ngrok running** in a separate terminal window while testing. If you restart ngrok, you'll get a new URL and need to update your webhook subscription.

## Step 3: Create a Webhook Subscription

Now subscribe to webhook events using the Deel API.

> **Tip**
>
> **Prefer a visual interface?** You can also create webhooks through the [Deel Developer Center](/api/webhooks/no-code) without writing code. This guide uses the API for learning purposes.

**`cURL`**

```bash cURL
curl -X POST 'https://api.letsdeel.com/rest/webhooks' \
  -H 'Authorization: Bearer YOUR_API_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "My first webhook",
    "description": "My first webhook",
    "status": "enabled",
    "url": "https://abc123.ngrok.io/webhooks/deel",
    "api_version": "v2",
    "events": ["contract.created", "contract.signed"]
  }'
```

**`Node.js`**

```javascript Node.js
const axios = require('axios');

async function createWebhook() {
  const response = await axios.post(
    'https://api.letsdeel.com/rest/webhooks',
    {
      name: 'My first webhook',
      description: 'My first webhook',
      status: 'enabled',
      url: 'https://abc123.ngrok.io/webhooks/deel',
      api_version: 'v2',
      events: ['contract.created', 'contract.signed']
    },
    {
      headers: {
        'Authorization': `Bearer ${process.env.DEEL_API_TOKEN}`,
        'Content-Type': 'application/json'
      }
    }
  );

  console.log('Webhook created:', response.data.data);
  console.log('Webhook ID:', response.data.data.id);
  console.log('Signing key:', response.data.data.signing_key); // Save this!
}

createWebhook();
```

**`Python`**

```python Python
import requests
import os

def create_webhook():
    response = requests.post(
        'https://api.letsdeel.com/rest/webhooks',
        json={
            'name': 'My first webhook',
            'description': 'My first webhook',
            'status': 'enabled',
            'url': 'https://abc123.ngrok.io/webhooks/deel',
            'api_version': 'v2',
            'events': ['contract.created', 'contract.signed']
        },
        headers={
            'Authorization': f'Bearer {os.getenv("DEEL_API_TOKEN")}',
            'Content-Type': 'application/json'
        }
    )

    data = response.json()['data']
    print(f'Webhook created: {data}')
    print(f'Webhook ID: {data["id"]}')
    print(f'Signing key: {data["signing_key"]}')  # Save this!

create_webhook()
```

> **Warning**
>
> **Save the signing key!** The API response includes a `signing_key` that you'll need for signature verification in the next step. Store it securely as an environment variable.

> **Tip**
>
> **Don't know which events to subscribe to?** List all available events first:
>
> ```bash
> curl 'https://api.letsdeel.com/rest/webhooks/events/types' \
>   -H 'Authorization: Bearer YOUR_API_TOKEN'
> ```

## Step 4: Add Signature Verification

Now add security to your webhook endpoint by verifying signatures:

**`Node.js`**

```javascript Node.js
// webhook-server.js
const express = require('express');
const crypto = require('crypto');

const app = express();
const SIGNING_KEY = process.env.DEEL_WEBHOOK_SECRET; // From Step 3

app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString();
  }
}));

function verifySignature(req) {
  const signature = req.headers['x-deel-signature'];
  const expectedSignature = crypto
    .createHmac('sha256', SIGNING_KEY)
    .update('POST' + req.rawBody)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

app.post('/webhooks/deel', (req, res) => {
  // Verify signature
  if (!verifySignature(req)) {
    console.log('❌ Invalid signature!');
    return res.status(401).send('Invalid signature');
  }

  console.log('✅ Signature verified!');
  console.log('Event:', req.body.data.meta.event_type);

  res.status(200).send('OK');
});

app.listen(3000, () => {
  console.log('🚀 Secure webhook server running');
});
```

**`Python`**

```python Python
# webhook_server.py
from flask import Flask, request, jsonify
import hmac
import hashlib
import os

app = Flask(__name__)
SIGNING_KEY = os.getenv('DEEL_WEBHOOK_SECRET')  # From Step 3

def verify_signature(request):
    signature = request.headers.get('x-deel-signature')
    raw_body = request.get_data(as_text=True)

    expected_signature = hmac.new(
        SIGNING_KEY.encode(),
        ('POST' + raw_body).encode(),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected_signature)

@app.route('/webhooks/deel', methods=['POST'])
def handle_webhook():
    # Verify signature
    if not verify_signature(request):
        print('❌ Invalid signature!')
        return jsonify({'error': 'Invalid signature'}), 401

    print('✅ Signature verified!')
    event = request.json
    print(f"Event: {event['data']['meta']['event_type']}")

    return jsonify({'status': 'success'}), 200

if __name__ == '__main__':
    app.run(port=3000, debug=True)
```

> **Note**
>
> **Security tip:** The signature is computed as `HMAC-SHA256(signing_key, "POST" + raw_body)`. Always prefix with "POST" before hashing!

## Step 5: Test It Out!

Now trigger a webhook event to see everything working:

#### Ensure everything is running

Make sure you have:

* ✅ Your webhook server running on port 3000
* ✅ ngrok tunnel active
* ✅ Webhook subscription created

#### Trigger an event in Deel Sandbox

Log into your [Deel Sandbox](https://app-sandbox.letsdeel.com) and perform an action that triggers your subscribed event:

* Create a new contract (triggers `contract.created`)
* Sign a contract (triggers `contract.signed`)
* Complete a payment (triggers `payment.completed`)

#### Check your server logs

You should see output like:

```
✅ Signature verified!
Event: contract.created
```

#### Verify in ngrok dashboard

Open the ngrok web interface at [http://localhost:4040](http://localhost:4040) to see the webhook request details

> **Tip**
>
> **Webhook not arriving?** Deel retries failed deliveries with exponential backoff. If your endpoint was down, the webhook will be retried automatically up to 10 times.

## What's in a Webhook Payload?

Here's what you'll receive when an event occurs:

```json
{
  "data": {
    "meta": {
      "event_type": "contract.created",
      "organization_id": "c2f26732-e747-4776-8a21-b31c379f2356"
    },
    "resource": [
      {
        "contract_id": "123abc1",
        "worker_email": "contractor@example.com",
        "status": "pending",
        "created_at": "2025-02-05T15:39:38.070Z"
      }
    ]
  },
  "timestamp": "2025-02-05T15:39:38.070Z"
}
```

**Key fields:**

* `data.meta.event_type` - The type of event that occurred
* `data.meta.organization_id` - Your organization ID
* `data.resource` - Event-specific data (varies by event type)
* `timestamp` - When the event occurred (ISO 8601 format)

## Common Issues & Solutions

#### Not receiving any webhooks

**Check:**

* Is your server running and accessible via ngrok?
* Did you use the correct ngrok HTTPS URL when creating the subscription?
* Are you triggering the right events you subscribed to?

**Quick test:**

```bash
curl -X POST https://abc123.ngrok.io/webhooks/deel \
  -H "Content-Type: application/json" \
  -d '{"test": "data"}'
```

#### Signature verification failing

**Common mistakes:**

* Not prefixing with "POST" before hashing
* Using parsed JSON instead of raw body
* Wrong signing key (check your environment variables)

**Debug it:**

```javascript
console.log('Received signature:', req.headers['x-deel-signature']);
console.log('Raw body:', req.rawBody);
console.log('Signing key:', SIGNING_KEY);
```

#### Getting 401 or 403 errors when creating webhook

**Possible causes:**

* Invalid or expired API token
* Using sandbox token for production API (or vice versa)

**Solution:**
Verify your token:

```bash
curl 'https://api.letsdeel.com/rest/organizations' \
  -H 'Authorization: Bearer YOUR_API_TOKEN'
```

## Learn More

#### Managing Webhooks

You can manage webhook subscriptions via API or through the Developer Center:

**Via API:**

* `GET /webhooks` - List all your webhooks
* `GET /webhooks/{id}` - Get webhook details
* `PATCH /webhooks/{id}` - Update webhook URL or events
* `DELETE /webhooks/{id}` - Delete webhook

**Via Developer Center:**
Visit the [Managing Webhooks](/api/webhooks/no-code) guide to learn how to create, view, edit, test, and monitor webhooks through the visual interface.

#### Webhook Retry Behavior

Deel retries failed webhooks up to 10 times with exponential backoff:

* 1st retry: 1 minute
* 2nd retry: 2 minutes
* 3rd retry: 4 minutes
* 4th-9th retries: Continue with exponential backoff
* 10th attempt: After 16 hours, webhook is disabled if it fails

After 10 failures, the webhook is automatically disabled and you'll need to re-enable it.

#### Production Best Practices

Before going live, make sure you:

* Always verify signatures with constant-time comparison
* Respond within 30 seconds (ideally under 5 seconds)
* Use HTTPS with a valid SSL certificate
* Log webhook deliveries for debugging
* Monitor for failures and retries
* Implement graceful error handling
* Set up alerts for webhook failures

## Additional Resources

#### [Events](/api/webhooks/events)

Learn about webhook events

#### [Simulations](/api/webhooks/simulations)

Simulate webhook events

#### [Endpoints](/api/endpoints/webhooks)

Webhook endpoints