> ## Documentation Index
> Fetch the complete documentation index at: https://developer.zapsterapi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> How webhooks work on the Zapster API. Receive WhatsApp events in real time, share one webhook across many instances, verify origin with custom headers, and handle retries.

### What are webhooks?

**Webhooks** are an efficient, automated way for one application to push data to another in real time. They let different systems communicate specific events without the receiving application having to poll for them. Instead, the application that generates the event sends a notification, usually an HTTP POST request, to a URL the receiving application configured in advance.

### What do webhooks do?

Webhooks notify your application about events that happen in another application or service. Some common tasks handled by webhooks include:

* **Real-time notifications**: the receiving application is notified immediately when something happens, such as a new order, a status change, or a new message.

* **Process automation**: they let you automate responses or actions in your application when certain events happen, without constantly querying the API.

* **System integration**: they make it easy to integrate different systems, letting events in one system trigger automatic actions in another.

### How do webhooks work?

1. **Setup**: first, the receiving application sets up an endpoint (a public URL) ready to receive webhook notifications.

2. **Webhook registration**: the sending application is configured to send notifications to the webhook endpoint whenever a specific event happens.

3. **Event delivery**: when the configured event happens, the sending application sends an HTTP POST request to the webhook endpoint, including the relevant event data in the request body.

4. **Event processing**: the receiving application processes the received information and can take several actions in response, such as updating a database, sending an email, or triggering another internal process.

```mermaid theme={null}
sequenceDiagram
    participant Zapster as Zapster API
    participant Webhook as Webhook Endpoint
    Zapster->>Webhook: Sends event (HTTP POST)
    Webhook-->>Zapster: Response (200 OK / Error >= 400)
    Zapster->>Webhook: Retry on error (up to 5 times)
```

### One webhook, many instances

A webhook on Zapster is a destination you register once (the URL that will receive events) and reuse across as many instances as you want. Each instance decides, on its own, which events it wants to receive at that destination.

It helps to understand two concepts that work together:

* **The webhook (on your account)**: holds the URL, a name, and the status (on or off). It is the address notifications are sent to. It belongs to your account, not to a specific instance.
* **The association with each instance**: every instance that uses the webhook gets its own association. That is where the subscribed events and per-instance settings live (enable/disable, test mode). The same webhook can be associated with several instances at once.

```mermaid theme={null}
flowchart LR
    W["Webhook<br/>(URL + name + status)"]
    W --> A["Instance A<br/>events: message.received"]
    W --> B["Instance B<br/>events: message.sent, message.read"]
    W --> C["Instance C<br/>events: instance.connected"]
```

#### Why share a webhook

The main reason is centralized maintenance. If you have dozens of instances sending events to the same system, registering the URL once and reusing it avoids repeating configuration. When the URL needs to change (new server, new domain, route adjustment), you update it in one place and the change applies to every instance that uses that webhook.

```mermaid theme={null}
flowchart LR
    E["You edit the URL<br/>once"] --> W["Webhook"]
    W --> A["Instance A"]
    W --> B["Instance B"]
    W --> C["Instance C"]
```

#### How to do it in practice

The same endpoint (`POST /wa/instances/:id/webhooks`) covers both paths. The difference is in the request body:

* Send **`url`** to create a new webhook and associate it with the instance right away. Use this for the first instance.
* Send **`webhook_id`** to reuse a webhook that already exists. Use this for the remaining instances.

<Warning>
  The body accepts **`url`** OR **`webhook_id`**, never both together. If both are sent, Zapster prioritizes `webhook_id`. The `events` field is always required and must have at least one event.
</Warning>

**1. First instance: create the webhook with `url`**

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.zapsterapi.com/v1/wa/instances/FIRST_INSTANCE/webhooks \
    -H "Authorization: Bearer YOUR_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://your-app.com/webhook",
      "name": "Production webhook",
      "events": ["message.received", "message.sent"]
    }'
  ```

  ```javascript JavaScript (client) theme={null}
  const response = await fetch(
    'https://api.zapsterapi.com/v1/wa/instances/FIRST_INSTANCE/webhooks',
    {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer YOUR_TOKEN',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        url: 'https://your-app.com/webhook',
        name: 'Production webhook',
        events: ['message.received', 'message.sent'],
      }),
    },
  );

  const data = await response.json();
  console.log(data);
  // The response returns the id of the created webhook, which you reuse on the next instances.
  // { "webhook_id": "2nenz69l0xbf0m3uu9tfo", ... }
  ```

  ```javascript JavaScript (server) theme={null}
  // Node.js 18+ (native fetch). Keep the token in an environment variable.
  const response = await fetch(
    'https://api.zapsterapi.com/v1/wa/instances/FIRST_INSTANCE/webhooks',
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.ZAPSTER_TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        url: 'https://your-app.com/webhook',
        name: 'Production webhook',
        events: ['message.received', 'message.sent'],
      }),
    },
  );

  const { webhook_id } = await response.json();
  console.log('Webhook created:', webhook_id);
  ```

  ```php PHP theme={null}
  <?php
  $ch = curl_init('https://api.zapsterapi.com/v1/wa/instances/FIRST_INSTANCE/webhooks');
  curl_setopt_array($ch, [
      CURLOPT_POST => true,
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER => [
          'Authorization: Bearer YOUR_TOKEN',
          'Content-Type: application/json',
      ],
      CURLOPT_POSTFIELDS => json_encode([
          'url' => 'https://your-app.com/webhook',
          'name' => 'Production webhook',
          'events' => ['message.received', 'message.sent'],
      ]),
  ]);

  $response = curl_exec($ch);
  curl_close($ch);

  $data = json_decode($response, true);
  echo $data['webhook_id'];
  ```

  ```go Go theme={null}
  package main

  import (
  	"bytes"
  	"encoding/json"
  	"fmt"
  	"io"
  	"net/http"
  )

  func main() {
  	body, _ := json.Marshal(map[string]any{
  		"url":    "https://your-app.com/webhook",
  		"name":   "Production webhook",
  		"events": []string{"message.received", "message.sent"},
  	})

  	req, _ := http.NewRequest(
  		http.MethodPost,
  		"https://api.zapsterapi.com/v1/wa/instances/FIRST_INSTANCE/webhooks",
  		bytes.NewReader(body),
  	)
  	req.Header.Set("Authorization", "Bearer YOUR_TOKEN")
  	req.Header.Set("Content-Type", "application/json")

  	resp, err := http.DefaultClient.Do(req)
  	if err != nil {
  		panic(err)
  	}
  	defer resp.Body.Close()

  	data, _ := io.ReadAll(resp.Body)
  	fmt.Println(string(data))
  }
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      "https://api.zapsterapi.com/v1/wa/instances/FIRST_INSTANCE/webhooks",
      headers={"Authorization": "Bearer YOUR_TOKEN"},
      json={
          "url": "https://your-app.com/webhook",
          "name": "Production webhook",
          "events": ["message.received", "message.sent"],
      },
  )

  data = response.json()
  print(data["webhook_id"])
  ```

  ```java Java theme={null}
  import java.net.URI;
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.http.HttpResponse;

  public class CreateWebhook {
      public static void main(String[] args) throws Exception {
          String body = """
              {
                "url": "https://your-app.com/webhook",
                "name": "Production webhook",
                "events": ["message.received", "message.sent"]
              }
              """;

          HttpRequest request = HttpRequest.newBuilder()
              .uri(URI.create("https://api.zapsterapi.com/v1/wa/instances/FIRST_INSTANCE/webhooks"))
              .header("Authorization", "Bearer YOUR_TOKEN")
              .header("Content-Type", "application/json")
              .POST(HttpRequest.BodyPublishers.ofString(body))
              .build();

          HttpResponse<String> response = HttpClient.newHttpClient()
              .send(request, HttpResponse.BodyHandlers.ofString());

          System.out.println(response.body());
      }
  }
  ```

  ```ruby Ruby theme={null}
  require "net/http"
  require "json"
  require "uri"

  uri = URI("https://api.zapsterapi.com/v1/wa/instances/FIRST_INSTANCE/webhooks")

  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true

  request = Net::HTTP::Post.new(uri)
  request["Authorization"] = "Bearer YOUR_TOKEN"
  request["Content-Type"] = "application/json"
  request.body = {
    url: "https://your-app.com/webhook",
    name: "Production webhook",
    events: ["message.received", "message.sent"]
  }.to_json

  response = http.request(request)
  data = JSON.parse(response.body)
  puts data["webhook_id"]
  ```
</CodeGroup>

**2. Remaining instances: reuse with `webhook_id`**

Use the `webhook_id` returned in the previous step to associate the same webhook with another instance. Note that the events can be different: each instance subscribes to what makes sense for it.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.zapsterapi.com/v1/wa/instances/SECOND_INSTANCE/webhooks \
    -H "Authorization: Bearer YOUR_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "webhook_id": "2nenz69l0xbf0m3uu9tfo",
      "events": ["message.received"]
    }'
  ```

  ```javascript JavaScript (client) theme={null}
  const response = await fetch(
    'https://api.zapsterapi.com/v1/wa/instances/SECOND_INSTANCE/webhooks',
    {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer YOUR_TOKEN',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        webhook_id: '2nenz69l0xbf0m3uu9tfo',
        events: ['message.received'],
      }),
    },
  );

  const data = await response.json();
  console.log(data);
  ```

  ```javascript JavaScript (server) theme={null}
  // Node.js 18+ (native fetch).
  const response = await fetch(
    'https://api.zapsterapi.com/v1/wa/instances/SECOND_INSTANCE/webhooks',
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.ZAPSTER_TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        webhook_id: '2nenz69l0xbf0m3uu9tfo',
        events: ['message.received'],
      }),
    },
  );

  console.log(await response.json());
  ```

  ```php PHP theme={null}
  <?php
  $ch = curl_init('https://api.zapsterapi.com/v1/wa/instances/SECOND_INSTANCE/webhooks');
  curl_setopt_array($ch, [
      CURLOPT_POST => true,
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER => [
          'Authorization: Bearer YOUR_TOKEN',
          'Content-Type: application/json',
      ],
      CURLOPT_POSTFIELDS => json_encode([
          'webhook_id' => '2nenz69l0xbf0m3uu9tfo',
          'events' => ['message.received'],
      ]),
  ]);

  $response = curl_exec($ch);
  curl_close($ch);

  echo $response;
  ```

  ```go Go theme={null}
  package main

  import (
  	"bytes"
  	"encoding/json"
  	"fmt"
  	"io"
  	"net/http"
  )

  func main() {
  	body, _ := json.Marshal(map[string]any{
  		"webhook_id": "2nenz69l0xbf0m3uu9tfo",
  		"events":     []string{"message.received"},
  	})

  	req, _ := http.NewRequest(
  		http.MethodPost,
  		"https://api.zapsterapi.com/v1/wa/instances/SECOND_INSTANCE/webhooks",
  		bytes.NewReader(body),
  	)
  	req.Header.Set("Authorization", "Bearer YOUR_TOKEN")
  	req.Header.Set("Content-Type", "application/json")

  	resp, err := http.DefaultClient.Do(req)
  	if err != nil {
  		panic(err)
  	}
  	defer resp.Body.Close()

  	data, _ := io.ReadAll(resp.Body)
  	fmt.Println(string(data))
  }
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      "https://api.zapsterapi.com/v1/wa/instances/SECOND_INSTANCE/webhooks",
      headers={"Authorization": "Bearer YOUR_TOKEN"},
      json={
          "webhook_id": "2nenz69l0xbf0m3uu9tfo",
          "events": ["message.received"],
      },
  )

  print(response.json())
  ```

  ```java Java theme={null}
  import java.net.URI;
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.http.HttpResponse;

  public class ReuseWebhook {
      public static void main(String[] args) throws Exception {
          String body = """
              {
                "webhook_id": "2nenz69l0xbf0m3uu9tfo",
                "events": ["message.received"]
              }
              """;

          HttpRequest request = HttpRequest.newBuilder()
              .uri(URI.create("https://api.zapsterapi.com/v1/wa/instances/SECOND_INSTANCE/webhooks"))
              .header("Authorization", "Bearer YOUR_TOKEN")
              .header("Content-Type", "application/json")
              .POST(HttpRequest.BodyPublishers.ofString(body))
              .build();

          HttpResponse<String> response = HttpClient.newHttpClient()
              .send(request, HttpResponse.BodyHandlers.ofString());

          System.out.println(response.body());
      }
  }
  ```

  ```ruby Ruby theme={null}
  require "net/http"
  require "json"
  require "uri"

  uri = URI("https://api.zapsterapi.com/v1/wa/instances/SECOND_INSTANCE/webhooks")

  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true

  request = Net::HTTP::Post.new(uri)
  request["Authorization"] = "Bearer YOUR_TOKEN"
  request["Content-Type"] = "application/json"
  request.body = {
    webhook_id: "2nenz69l0xbf0m3uu9tfo",
    events: ["message.received"]
  }.to_json

  puts http.request(request).body
  ```
</CodeGroup>

#### Editing and removing: what changes

Because the webhook and the association are distinct things, editing or removing has different scopes. It is worth knowing each operation before applying changes in production.

| Operation              | Endpoint                                                             | Scope    | Effect                                                          |
| ---------------------- | -------------------------------------------------------------------- | -------- | --------------------------------------------------------------- |
| Create + associate new | `POST /wa/instances/:id/webhooks` with `url`                         | Instance | Creates a new webhook and associates it with the instance       |
| Reuse existing         | `POST /wa/instances/:id/webhooks` with `webhook_id`                  | Instance | Associates an existing webhook with one more instance           |
| Edit the webhook       | `PATCH /webhooks/:id` (url/name/enabled)                             | Account  | Propagates to ALL associated instances                          |
| Edit the association   | `PATCH /wa/instances/:id/webhooks/:whId` (events/enabled/test\_mode) | Instance | Changes only that instance's subscription                       |
| Disassociate           | `DELETE /wa/instances/:id/webhooks/:whId`                            | Instance | Turns it off for that instance only; still active on the others |
| Delete                 | `DELETE /webhooks/:id`                                               | Account  | Removes it from ALL instances                                   |

<Warning>
  **Editing the webhook propagates to everyone.** Changing the URL or the name through the account endpoint (`PATCH /webhooks/:id`) affects every associated instance. If you need a different URL for a single instance, create a new webhook instead of editing the existing one.
</Warning>

<Note>
  **Disassociating is not the same as deleting.** `DELETE /wa/instances/:id/webhooks/:whId` only turns the webhook off for that instance; it stays active on the others and on your account. To delete the webhook for good, use `DELETE /webhooks/:id`.
</Note>

#### How to tell the origin apart on the receiver

Each instance delivers events independently, even when they share the same webhook. To know where each notification came from, use the HTTP headers:

* **`X-Instance-ID`**: identifies the instance that generated the event (the real origin).
* **`X-Webhook-ID`**: identifies the shared webhook that delivered the notification.

Because the subscribed events can differ per instance, the same webhook may receive `message.received` from one instance and `message.sent` from another. Always look at `X-Instance-ID` to route or log the event correctly.

#### When to share and when to separate

Sharing a webhook makes sense when:

* All instances deliver to the same system (a CRM, a queue, a central endpoint).
* You want to change the destination URL in one place in the future.
* The processing on the receiver already uses `X-Instance-ID` to separate origins.

Separate webhooks per instance make more sense when:

* Each instance belongs to a different customer or product, with its own URL.
* You need to turn a single instance's destination on or off without touching the others.
* Separate environments (production and staging) must not mix.

#### Best practices

* Store the `webhook_id` returned at creation; it is what you reuse on the next instances.
* Use the webhook `name` to make its purpose clear (for example, "CRM production").
* On the receiver, treat `X-Instance-ID` as the source of truth for the event's origin.
* Before editing the URL of a shared webhook, confirm which instances will be affected with the [list webhooks endpoint](/pt-BR/v1/api-reference/webhooks/list-webhooks).

#### Frequently asked questions

<AccordionGroup>
  <Accordion title="Can I change one instance's URL without affecting the others?">
    Not through the webhook edit endpoint, which is account-scoped and propagates to every associated instance. For a dedicated URL, create a new webhook by sending `url` at creation and associate it with the desired instance only.
  </Accordion>

  <Accordion title="If I disassociate a webhook from an instance, does it disappear from the account?">
    No. `DELETE /wa/instances/:id/webhooks/:whId` only turns the webhook off for that instance. It stays active on the others and on your account. To delete it for good, use `DELETE /webhooks/:id`.
  </Accordion>

  <Accordion title="Do instances that share a webhook have to subscribe to the same events?">
    No. Events are defined per instance, in the association. One can subscribe to `message.received` and another to `message.sent`, even pointing to the same webhook.
  </Accordion>

  <Accordion title="How do I know which instance sent each notification?">
    Through the `X-Instance-ID` header, present in every notification. `X-Webhook-ID` indicates the webhook that made the delivery.
  </Accordion>

  <Accordion title="Can I send `url` and `webhook_id` in the same request?">
    Send only one of them. If both are provided, Zapster prioritizes `webhook_id` and ignores `url`.
  </Accordion>
</AccordionGroup>

For the details of each endpoint, check the API reference: [create/associate webhook](/pt-BR/v1/api-reference/instance/create-webhook), [edit the instance association](/pt-BR/v1/api-reference/instance/update-webhook), [disassociate from the instance](/pt-BR/v1/api-reference/instance/delete-webhook), [edit the webhook](/pt-BR/v1/api-reference/webhooks/update-webhook), and [delete the webhook](/pt-BR/v1/api-reference/webhooks/delete-webhook).

### Failure handling and retries

In an ideal scenario, the receiving application receives and processes the webhook request without issues. However, failures can happen for various reasons, such as server downtime, network problems, or processing errors.

To make sure important notifications are not lost, we implement a **retry** mechanism. If the receiving application responds with an HTTP status code greater than 400 (indicating an error), the sending application will retry the webhook notification up to **5 times**.

#### Retry details

* **Failure criterion**: any response with an HTTP status code greater than or equal to 400.
* **Number of retries**: up to 5 attempts.
* **Interval between retries**: the interval between each retry grows progressively with a factor of 2.5. The intervals in seconds are:

  * **1st attempt:** 2.5 seconds (`2.5^1`)
  * **2nd attempt:** \~6 seconds (`2.5^2`)
  * **3rd attempt:** \~15 seconds (`2.5^3`)
  * **4th attempt:** \~39 seconds (`2.5^4`)
  * **5th attempt:** \~97 seconds (`2.5^5`)

  Each interval is calculated as `2.5^n`, where `n` is the attempt number.

### Custom HTTP headers

Every webhook notification sent by the Zapster API includes custom HTTP headers that identify the origin and context of the event. These headers are useful for validation, logging, and firewall rules.

| Header            | Type   | Description                                 | Example            | Present in                   |
| ----------------- | ------ | ------------------------------------------- | ------------------ | ---------------------------- |
| `X-Instance-ID`   | string | ID of the instance that generated the event | `inst_abc123`      | Every notification           |
| `X-Message-ID`    | string | Unique ID of the notification               | `msg_xyz789`       | Every notification           |
| `X-Webhook-ID`    | string | ID of the registered webhook                | `whk_def456`       | When a webhook is registered |
| `X-Attempt-Count` | number | Attempt number (1-5)                        | `1`                | Every notification           |
| `User-Agent`      | string | Sender identifier                           | `Zapsterapi/1.2.3` | Every notification           |

### Validation and allowlisting

You can use the custom HTTP headers to validate the origin of received notifications. Because the webhook is delivered via `POST` to your server, the examples below show the receiving endpoint in each language (cURL and browser code do not receive webhooks, so they are not shown here):

<CodeGroup>
  ```javascript JavaScript (server) theme={null}
  // Node.js + Express
  app.post('/webhook', (req, res) => {
    const instanceId = req.headers['x-instance-id']
    const messageId = req.headers['x-message-id']
    const attemptCount = req.headers['x-attempt-count']

    console.log(`Notification received from instance ${instanceId}`)
    console.log(`Message ID: ${messageId}, attempt: ${attemptCount}`)

    // Validate the origin before processing
    if (!instanceId) {
      return res.status(400).json({ error: 'Missing X-Instance-ID header' })
    }

    // Process the event
    const event = req.body
    console.log(`Event: ${event.type}`, event.data)

    res.status(200).json({ received: true })
  })
  ```

  ```php PHP theme={null}
  <?php
  // Headers arrive in $_SERVER with the HTTP_ prefix and underscores instead of hyphens
  $instanceId = $_SERVER['HTTP_X_INSTANCE_ID'] ?? null;
  $messageId = $_SERVER['HTTP_X_MESSAGE_ID'] ?? null;
  $attemptCount = $_SERVER['HTTP_X_ATTEMPT_COUNT'] ?? null;

  error_log("Notification received from instance {$instanceId}");
  error_log("Message ID: {$messageId}, attempt: {$attemptCount}");

  // Validate the origin before processing
  if (!$instanceId) {
      http_response_code(400);
      echo json_encode(['error' => 'Missing X-Instance-ID header']);
      exit;
  }

  // Process the event
  $event = json_decode(file_get_contents('php://input'), true);
  error_log("Event: {$event['type']}");

  http_response_code(200);
  echo json_encode(['received' => true]);
  ```

  ```go Go theme={null}
  package main

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

  func webhookHandler(w http.ResponseWriter, r *http.Request) {
  	instanceID := r.Header.Get("X-Instance-ID")
  	messageID := r.Header.Get("X-Message-ID")
  	attemptCount := r.Header.Get("X-Attempt-Count")

  	log.Printf("Notification received from instance %s", instanceID)
  	log.Printf("Message ID: %s, attempt: %s", messageID, attemptCount)

  	// Validate the origin before processing
  	if instanceID == "" {
  		w.WriteHeader(http.StatusBadRequest)
  		json.NewEncoder(w).Encode(map[string]string{"error": "Missing X-Instance-ID header"})
  		return
  	}

  	// Process the event
  	var event map[string]any
  	json.NewDecoder(r.Body).Decode(&event)
  	log.Printf("Event: %v", event["type"])

  	w.WriteHeader(http.StatusOK)
  	json.NewEncoder(w).Encode(map[string]bool{"received": true})
  }
  ```

  ```python Python theme={null}
  from flask import Flask, request, jsonify

  app = Flask(__name__)

  @app.post("/webhook")
  def webhook():
      instance_id = request.headers.get("X-Instance-ID")
      message_id = request.headers.get("X-Message-ID")
      attempt_count = request.headers.get("X-Attempt-Count")

      print(f"Notification received from instance {instance_id}")
      print(f"Message ID: {message_id}, attempt: {attempt_count}")

      # Validate the origin before processing
      if not instance_id:
          return jsonify(error="Missing X-Instance-ID header"), 400

      # Process the event
      event = request.get_json()
      print(f"Event: {event['type']}", event["data"])

      return jsonify(received=True), 200
  ```

  ```java Java theme={null}
  import org.springframework.http.ResponseEntity;
  import org.springframework.web.bind.annotation.*;
  import java.util.Map;

  @RestController
  public class WebhookController {

      @PostMapping("/webhook")
      public ResponseEntity<?> receive(
              @RequestHeader(value = "X-Instance-ID", required = false) String instanceId,
              @RequestHeader(value = "X-Message-ID", required = false) String messageId,
              @RequestHeader(value = "X-Attempt-Count", required = false) String attemptCount,
              @RequestBody Map<String, Object> event) {

          System.out.printf("Notification received from instance %s%n", instanceId);
          System.out.printf("Message ID: %s, attempt: %s%n", messageId, attemptCount);

          // Validate the origin before processing
          if (instanceId == null) {
              return ResponseEntity.badRequest().body(Map.of("error", "Missing X-Instance-ID header"));
          }

          // Process the event
          System.out.println("Event: " + event.get("type"));

          return ResponseEntity.ok(Map.of("received", true));
      }
  }
  ```

  ```ruby Ruby theme={null}
  require "sinatra"
  require "json"

  post "/webhook" do
    instance_id = request.env["HTTP_X_INSTANCE_ID"]
    message_id = request.env["HTTP_X_MESSAGE_ID"]
    attempt_count = request.env["HTTP_X_ATTEMPT_COUNT"]

    puts "Notification received from instance #{instance_id}"
    puts "Message ID: #{message_id}, attempt: #{attempt_count}"

    # Validate the origin before processing
    halt 400, { error: "Missing X-Instance-ID header" }.to_json if instance_id.nil?

    # Process the event
    event = JSON.parse(request.body.read)
    puts "Event: #{event['type']}"

    content_type :json
    { received: true }.to_json
  end
  ```
</CodeGroup>

**Allowlisting in a WAF/firewall:** if you use a Web Application Firewall (WAF) or firewall rules, configure it to accept `POST` requests that carry the `X-Instance-ID`, `X-Message-ID`, `X-Attempt-Count`, and `User-Agent` headers with the `Zapsterapi/` prefix.
