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

# Webhooks Overview

> Payload envelope, signature verification, top-level fields, and links to per-product event pages.

<Note>
  Webhooks will only be delivered to accounts set as `Admin` or `Developer` roles within the Dashboard.
</Note>

Entri uses webhooks to notify your backend about the status of a domain that has been [connected](/getting-started) or [sold](/sell/overview). You can configure a URL in the Entri dashboard to receive these webhook events.

## Example webhook payload

When a domain has been added by a user, Entri sends the following webhook payload:

```json theme={null}
{
  "id": "1234567890-abcdefg-1234567890",
  "user_id": "your-provided-user-id",
  "domain": "example.com",
  "subdomain": "shop",
  "provider" : "cloudflare",
  "type": "domain.added",
  "propagation_status": "pending",
  "dkim_status": "success",
  "redirection_status": "exempt",
  "setup_type": "automatic",
  "secure_status": "success",
  "power_status": "success",
  "monitor_status": "pending",
  "dmarc_updated": "exempt",
  "cname_target": "my.saascompany.com",
  "purchased_domains": ["domain1.com", "domain2.com", "domainN.com"],
  "free_domain": true,
  "data": {
    "records_propagated": [
      {
        "type": "A",
        "host": "smallbusiness.com",
        "value": "54.153.2.220",
        "applicationUrl": "www.example.com",
        "powerRootPathAccess": ["path1", "path2", "pathN"]
      },
      {
        "type": "CNAME",
        "host": "www",
        "value": "smallbusiness.com"
      }
    ],
    "records_non_propagated": []
  }
}
```

## Webhook payload breakdown

The webhook payload includes several fields with nested objects, depending on the type of event. Below is a complete description of each key and value.

### Top-Level fields

* **`id`**: A unique identifier for the webhook event (e.g. `"1234567890-abcdefg-1234567890"`).
* **`user_id`**: The ID of the user who initiated the domain-related action (e.g. `"your-provided-user-id"`).
* **`domain`**: The domain involved in the event (e.g. `"example.com"`).
* **`subdomain`**: The subdomain associated with the event, if any (e.g. `"shop"`).
* **`provider`**: The domain's provider associated with the event (e.g. `"cloudflare"`).

For event types and status fields, see the per-product events pages: [Connect events](/connect/events), [Sell events](/sell/events), [Power events](/power/events), [Secure events](/secure/events), [Monitor events](/monitor/events).

## Examples of key and value pairs

* `"type": "domain.added"`: An event type indicating that a domain has been added.
* `"propagation_status": "pending"`: DNS record propagation is still in progress.
* `"secure_status": "success"`: SSL configuration completed successfully.
* `"power_status": "failed"`: The advanced feature configuration process has failed.
* `"cname_target": "my.saascompany.com"`: The CNAME target for Entri Power or Secure configuration.

## Webhook updates

If any part of the webhook object is updated, subsequent webhooks will include ALL fields. Example update:

```json theme={null}
{
  "id": "1234567890-abcdefg-1234567890",
  "propagation_status": "success",
  "updated_objects": ["propagation_status"]
}
```

## Propagation retries

If the DNS records are not fully propagated on the first attempt, Entri will retry every 10 seconds for the first 2 mins, and then at increasing intervals (1 minute, 2 minutes, 4 minutes, 8 mins, ...etc). This process will continue for up to 72 hours. After 72 hours, if the records are still not propagated, a webhook with `"type": "domain.propagation.timeout"` will be sent.

## Connection failures retries

Webhooks automatically retries up to 3 times if something goes wrong. If all 3 retries fail, a warning email will be sent (limited to 1 per day).

## DKIM status notes

The `dkim_status` field is set to `exempt` if:

* The user does not use Google or Microsoft email providers.
* The `Enable support for DKIM` setting is disabled for your `applicationId`.

## Overview and Webhook Structure

<Check>**Included in all tiers.**</Check>

All webhook notifications include the following headers and follow the structure described below.

All webhook notifications will have the following header in the Request: `"User-Agent": "Entri-Webhook"`.

This is the list of keys you will receive on a webhook notification:

```JSON JSON theme={null}
{
  "id": "028b5078-8fed-4ffb-8e3e-3e6e6d8214b4",
  "user_id": "sample-user",
  "domain": "smallbusiness.com",
  "provider": "ionos.com",
  "type": "domain.added",
  "propagation_status": "success",
  "dkim_status": "success",
  "redirection_status": "exempt",
  "ssl_status": "success",
  "setup_type": "automatic",
  "secure_status": "success", //Only for Entri Secure usage
  "power_status" : "success", //Only for Entri Power usage
  "cname_target": "my.saascompany.com", //Only for Entri Power and Secure usage
  "purchased_domains": ["domain1.com",...,"domainN.com"], //Only for Entri Sell usage
  "data": {
    "records_propagated": [
      {
        "type": "A",
        "host": "smallbusiness.com",
        "value": "54.153.2.220"
      },
      {
        "type": "CNAME",
        "host": "www",
        "value": "smallbusiness.com"
      }
    ],
    "records_non_propagated": [],
    "updated_objects": [
      "propagation_status",
      "ssl_status"
    ]
  }
}
```

The JSON object contains the following properties:

| Property                      | Type                                                          | Description                                                                                                                                                                                         |
| ----------------------------- | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| id                            | string                                                        | Entri's ID for the webhook event                                                                                                                                                                    |
| user\_id                      | string                                                        | The user ID that your application can optionally send via the configuration object during setup (see the [entri.showEntri method](/connect/configuration#entrishowentriconfig))                     |
| domain                        | string                                                        | The domain name set by your user                                                                                                                                                                    |
| type                          | \[domain.purchased, domain.added, domain.propagation.timeout] | The event type. Options:- domain.added- domain.propagation.timeout- domain.purchased                                                                                                                |
| setup\_type                   | \[automatic, manual]                                          | States if the dns record(s) was done in a manual or automatic way. Options:- automatic- manual                                                                                                      |
| propagation\_status           | \[pending, success, failed, exempt]                           | The DNS propagation status. Options:- success- pending- failed                                                                                                                                      |
| dkim\_status                  | \[pending, success, failed, exempt]                           | Whether DKIM has been set up (see [Handling DKIM, SPF, and DMARC Records](/handling-dkim-spf-dmarc-records)). Options:- success- pending- failed- exempt (the feature is disabled)                  |
| redirection\_status           | \[pending, success, failed, exempt]                           | If the wwwRedirect feature was enabled for your applicationId, then Entri will check to confirm the status of the url redirect.Options:- success- pending- failed- exempt (the feature is disabled) |
| data.records\_propagated      | array of DNSRecord objects                                    | See above                                                                                                                                                                                           |
| data.records\_non\_propagated | array of DNSRecord objects                                    | See above                                                                                                                                                                                           |
| updated\_objects              | array of strings                                              | The object properties that have been updated since the last webhook event.                                                                                                                          |
| secure\_status                | \[pending, success, failed, exempt]                           | Whether an SSL certificate has been provisioned (see [Provisioning SSL Certificates](/secure/overview)). Options:- success- pending- failed- exempt (the feature is disabled)                       |
| power\_status                 | \[pending, success, failed, exempt]                           | Whether a domain has been powered (see [Powering domains](/power/overview)). Options:- success- pending- failed- exempt (the feature is disabled)                                                   |
| cname\_target                 | string                                                        | Account's configured cname\_target value for Entri Power and Secure or empty string if it doesn't apply.                                                                                            |
| purchased\_domains            | array of strings                                              | List of purchased domains along the Entri Sell flow or empty array if not an Entri Sell flow.                                                                                                       |
| provider                      | string                                                        | The domain's provider associated with the event (e.g. `"cloudflare"`).                                                                                                                              |

## Webhook Management Endpoints

These endpoints are designed to help partners retrieve or re-trigger webhook events programmatically.
Each request requires a valid jobId, which can be captured from the [`onSuccess`](/connect/events#onsuccess-callback-event) browser event.

### Trigger propagation re-check (async)

When this endpoint is called, Entri performs an immediate DNS propagation re-check for the specified Connect flow and **delivers the latest result via webhook**. The endpoint is still **asynchronous**: it only confirms that the check was queued; the propagation result is sent exclusively through the webhook, and no continuous propagation checks happen after this manual re-check.

> **Tip:** The `jobId` required for this endpoint can be obtained from the [`onSuccess`](/connect/events#onsuccess-callback-event) browser event once the flow finishes successfully.

```txt theme={null}
POST https://api.goentri.com/connect/propagation/recheck/:job_id
Header "Authorization: [JWT authorization]"
Header "applicationId: [yourApplicationID]"
```

#### Alternative UI-based option

This action can also be triggered directly from the Customer Dashboard, under the Webhook logs section.
In front of each log, users will find the “Check propagation again” button, which performs the same propagation re-check as this endpoint.

#### Headers

| Header        | Required? | Description                                  |
| ------------- | --------- | -------------------------------------------- |
| Authorization | Yes       | Bearer token (JWT) for authentication.       |
| applicationId | Yes       | Your Application ID as set in the dashboard. |

#### Path Parameters

| Parameter | Type          | Required? | Description                                                                                |
| --------- | ------------- | --------- | ------------------------------------------------------------------------------------------ |
| job\_id   | string (UUID) | Yes       | The Connect flow identifier (returned by initialize and included in all related webhooks). |

#### Request Body

*None.* All inputs are provided via path parameters and headers.

#### Response

* **202 Accepted** if the re-check was queued successfully.

```json theme={null}
{
  "jobId": "9c128fe4-63cd-4ec4-9ae8-8a9d06c0e6de",
  "queued": true,
  "message": "Propagation re-check has been queued. A webhook will be sent with the result.",
  "enqueuedAt": "2025-11-05T18:22:00Z"
}
```

#### Webhook behavior

* Performs one immediate propagation re-check and sends a webhook with the **latest** propagation status; no continuous propagation retries follow this manual re-check.
* Includes `"manual_retry": true` in the webhook **body**.
* Follows the standard delivery policy with up to **3 attempts** if the receiver is unavailable.
* **Logs a new webhook attempt** entry; existing webhook logs are not overridden.
* Uses the same execution codepath as the private Dashboard action for this functionality.

#### Cooldown

To avoid abuse and accidental hot-polling, this endpoint can be called **once every 5 minutes per job**.\
If called sooner, it returns:

* **429 Too Many Requests** with message: `"You can only trigger the check once every 5 mins"`\
  (Servers may also include `Retry-After: 300`.)

#### Error Responses

| HTTP | Example Body                                                        | Description                                                  |
| ---: | ------------------------------------------------------------------- | ------------------------------------------------------------ |
|  400 | `{ "message": "Invalid job_id format" }`                            | Malformed UUID.                                              |
|  401 | `{ "message": "Unauthorized" }`                                     | Invalid or missing JWT.                                      |
|  403 | `{ "message": "Forbidden" }`                                        | The `job_id` does not belong to the calling `applicationId`. |
|  404 | `{ "message": "Job not found" }`                                    | Unknown `job_id`.                                            |
|  429 | `{ "message": "You can only trigger the check once every 5 mins" }` | Called within the 5‑minute cooldown window.                  |
|  500 | `{ "message": "Internal Server Error" }`                            | Unexpected server error.                                     |

#### Example cURL

```bash theme={null}
curl -s -X POST \
  "https://api.goentri.com/connect/propagation/recheck/9c128fe4-63cd-4ec4-9ae8-8a9d06c0e6de" \
  -H "Authorization: Bearer <JWT>" \
  -H "applicationId: <YOUR_APP_ID>"
```

### Get last webhook by job ID

This endpoint allows you to retrieve the last webhook event sent for a specific Connect flow, based on the `job_id`. It is useful for debugging or validating the final propagation result of a specific domain connection.

> **Tip:** You can obtain the `jobId` value directly from the [`onSuccess`](/connect/events#onsuccess-callback-event) browser event, which is triggered when the user reaches the Congratulations screen.

```txt theme={null}
GET https://api.goentri.com/connect/webhooks/last/:job_id
Header "Authorization: [JWT authorization]"
Header "applicationId: [yourApplicationID]"
```

#### Headers

| Header        | Required? | Description                                  |
| ------------- | --------- | -------------------------------------------- |
| Authorization | Yes       | Bearer token (JWT) for authentication.       |
| applicationId | Yes       | Your Application ID as set in the dashboard. |

#### Path Parameters

| Parameter | Type          | Required? | Description                                                                                                              |
| --------- | ------------- | --------- | ------------------------------------------------------------------------------------------------------------------------ |
| job\_id   | string (UUID) | Yes       | The UUID of the Connect flow. The same job\_id returned in the initialize endpoint and included in all related webhooks. |

#### Sample Response

```json theme={null}
{
  "id": "d6a6b6c4-45f3-42a2-8bb1-3c92debf47e2",
  "job_id": "9c128fe4-63cd-4ec4-9ae8-8a9d06c0e6de",
  "domain": "example.com",
  "type": "domain.propagation.success",
  "manual_retry": false,
  "status": "delivered",
  "sentAt": "2025-11-05T18:22:00Z",
  "payload": {
    "domain": "example.com",
    "job_id": "9c128fe4-63cd-4ec4-9ae8-8a9d06c0e6de",
    "propagation_status": "success",
    "records_checked": [
      { "type": "CNAME", "host": "www", "value": "myapp.com" }
    ]
  }
}
```

#### Error Responses

| HTTP | Example Body                             | Description                                               |
| ---- | ---------------------------------------- | --------------------------------------------------------- |
| 400  | `{ "message": "Invalid job_id format" }` | Malformed or missing job\_id.                             |
| 401  | `{ "message": "Unauthorized" }`          | Invalid or missing JWT.                                   |
| 403  | `{ "message": "Forbidden" }`             | The job\_id does not belong to the calling applicationId. |
| 404  | `{ "message": "Webhook not found" }`     | No webhook log was found for the given job\_id.           |
| 500  | `{ "message": "Internal Server Error" }` | Unexpected server error.                                  |

#### Notes

* This endpoint is **read-only** and returns the last logged webhook event associated with the provided job\_id.
* It retrieves data from the webhook logs table and does **not** resend or trigger new webhooks.
* Only returns the **most recent** webhook entry for that job.

## Provider Health

Returns the status of all the providers for an application ID that Entri supports.

```
GET https://api.goentri.com/providers_health
Header "Authorization: [JWT authorization]"
Header "applicationId: [yourApplicationID]"
```

### Response body

```JSON JSON theme={null}
{
  "message": "success",
  "data": [
    {
      "provider": "IONOS",
      "enabled": true
    },
    {
      "provider": "Enom",
      "enabled": true
    },
    {
      "provider": "Amazon Route 53",
      "enabled": true
    },
    ...
  ]
}
```

## Entri Webhook Signature Verification

## Enforcing Webhook Source IP (Optional)

All webhook requests from Entri are now sent from a fixed static IP address:

`3.14.77.245`

For an additional layer of security, you may allow webhook traffic only from this IP within your firewall, API gateway, or reverse proxy. This ensures that only Entri can reach your webhook endpoint, even before signature validation occurs, providing a strong network-level safeguard against unauthorized requests.

<Note>
  The fixed IP address (`3.14.77.245`) applies only to production webhook deliveries. The **Dashboard webhook tester** does not originate from this IP address. If you are testing webhooks using the Dashboard tester, you must temporarily disable IP allowlisting or your endpoint may reject the request.
</Note>

## Signature Timestamp Header

The `Entri-Timestamp` header contains the Unix timestamp (in seconds) for when the webhook was generated. It is used by both Signature V2 and Signature V3 verification. This enables servers to enforce freshness windows and reject replayed requests.

## Signature Verification Methods

Entri sends all three signature headers on every webhook request — they are additive and do not replace each other. You may verify any or all of them; we recommend verifying at least V3 for new integrations.

| Header               | Algorithm              | Signed content                                                 | Notes                                          |
| -------------------- | ---------------------- | -------------------------------------------------------------- | ---------------------------------------------- |
| `Entri-Signature`    | SHA-256                | `sha256(webhook_id + client_secret)`                           | V1 — legacy                                    |
| `Entri-Signature-V2` | SHA-256                | `sha256(webhook_id + timestamp + client_secret)`               | Replay-protected                               |
| `Entri-Signature-V3` | HMAC-SHA256 (RFC 2104) | `hmac_sha256(key=client_secret, msg="{timestamp}.{raw_body}")` | **Recommended** — body-bound, replay-protected |
| `Entri-Timestamp`    | —                      | Unix timestamp (seconds)                                       | Used by V2 and V3                              |

### Signature V3 (Recommended)

`Entri-Signature-V3` uses true **HMAC-SHA256** (RFC 2104). It provides two security improvements over V1 and V2:

* **Body binding**: the HMAC is computed over the raw HTTP request body, so any tampering with the payload invalidates the signature. V1 and V2 sign only identifiers and timestamps, not the body contents.
* **Replay protection**: the `Entri-Timestamp` value is embedded in the signed message, so a captured valid request cannot be replayed after 300 seconds.

#### Header value format

```
Entri-Signature-V3: sha256=<lowercase hex digest>
```

#### How to verify

1. Read `Entri-Timestamp`. Reject the request if `abs(now - timestamp) > 300` seconds.
2. Build the signed payload by concatenating: the timestamp string, a literal `.`, then the **raw HTTP request body bytes**.
3. Compute `HMAC-SHA256` with your `client_secret` as the key over that payload.
4. Hex-encode the result and prepend `sha256=`.
5. Compare to the `Entri-Signature-V3` header value using a constant-time comparison.

<Warning>
  Always compute the HMAC over the **raw HTTP request body bytes**, before any JSON parsing. Do not parse the JSON body and re-serialize it — different libraries produce different byte sequences (whitespace, key ordering, number formatting), which will cause verification to fail.
</Warning>

#### Verification examples

<CodeGroup>
  ```python Python theme={null}
  import hmac
  import hashlib
  import time

  def verify_entri_signature_v3(
      raw_body: bytes,
      headers: dict,
      client_secret: str,
      max_age_seconds: int = 300,
  ) -> bool:
      timestamp = headers.get("Entri-Timestamp", "")
      signature = headers.get("Entri-Signature-V3", "")

      try:
          if abs(time.time() - int(timestamp)) > max_age_seconds:
              return False
      except (ValueError, TypeError):
          return False

      signed_payload = f"{timestamp}.".encode() + raw_body
      expected = "sha256=" + hmac.new(
          key=client_secret.encode(),
          msg=signed_payload,
          digestmod=hashlib.sha256,
      ).hexdigest()

      return hmac.compare_digest(expected, signature)
  ```

  ```javascript Node.js theme={null}
  const crypto = require("crypto");

  function verifyEntriSignatureV3(rawBody, headers, clientSecret, maxAgeSeconds = 300) {
    // Header names are lowercased by most Node.js HTTP frameworks
    const timestamp = headers["entri-timestamp"] ?? headers["Entri-Timestamp"] ?? "";
    const signature = headers["entri-signature-v3"] ?? headers["Entri-Signature-V3"] ?? "";

    const ts = parseInt(timestamp, 10);
    if (isNaN(ts) || Math.abs(Date.now() / 1000 - ts) > maxAgeSeconds) {
      return false;
    }

    // rawBody must be the original Buffer — do not pass a re-serialized JSON string
    const body = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody);
    const signedPayload = Buffer.concat([Buffer.from(`${timestamp}.`), body]);
    const expected = "sha256=" + crypto
      .createHmac("sha256", clientSecret)
      .update(signedPayload)
      .digest("hex");

    try {
      return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
    } catch {
      return false;
    }
  }
  ```

  ```php PHP theme={null}
  <?php

  function verifyEntriSignatureV3(
      string $rawBody,
      array $headers,
      string $clientSecret,
      int $maxAgeSeconds = 300
  ): bool {
      // PHP header arrays may use different casing depending on the framework
      $timestamp = $headers[‘Entri-Timestamp’] ?? $headers[‘entri-timestamp’] ?? ‘’;
      $signature  = $headers[‘Entri-Signature-V3’] ?? $headers[‘entri-signature-v3’] ?? ‘’;

      if (!is_numeric($timestamp) || abs(time() - (int) $timestamp) > $maxAgeSeconds) {
          return false;
      }

      // Concatenate timestamp, literal dot, and raw body — do not JSON-decode first
      $signedPayload = $timestamp . ‘.’ . $rawBody;
      $expected = ‘sha256=’ . hash_hmac(‘sha256’, $signedPayload, $clientSecret);

      return hash_equals($expected, $signature);
  }
  ```
</CodeGroup>

<Note>
  `Entri-Signature` (V1) and `Entri-Signature-V2` continue to be sent alongside V3. Existing integrations that verify V1 or V2 will keep working without any changes. V3 is purely additive — add it to new integrations and migrate existing ones at your convenience.
</Note>

### Signature V2

`Entri-Signature-V2` adds timestamp binding to V1, making each signature unique and time-bound. It is computed as `SHA256(webhook_id + timestamp + client_secret)` with no separators between the values.

<Note>
  The V2 signature is a plain SHA-256 digest of `webhook_id ‖ timestamp ‖ client_secret`. It is **not** an RFC 2104 HMAC-SHA256. Placing the secret at the end of the input avoids the length-extension weakness of the `H(secret ‖ msg)` construction.
</Note>

#### How to verify

1. Read the webhook payload’s `id` field.
2. Read the `Entri-Timestamp` header (epoch seconds).
3. Build the canonical message: `webhook_id + timestamp + client_secret` (concatenated, no separators).
4. Compute `SHA256(canonical_message)` and hex-encode it.
5. Compare the result to the `Entri-Signature-V2` header using a constant-time comparison.
6. Enforce a freshness window (for example, 5 minutes) using the timestamp.

#### Verification example (Python)

```python theme={null}
import hashlib
import hmac
import time

def verify_entri_signature_v2(
    webhook_id: str,
    client_secret: str,
    received_signature: str,
    received_timestamp: str,
    max_age_seconds: int = 300,
) -> bool:
    try:
        if int(time.time()) - int(received_timestamp) > max_age_seconds:
            return False

        expected = hashlib.sha256(
            webhook_id.encode() + received_timestamp.encode() + client_secret.encode()
        ).hexdigest()

        return hmac.compare_digest(expected, received_signature)
    except (ValueError, TypeError, AttributeError):
        return False
```

### Signature V1 (Legacy)

`Entri-Signature` is the original signature, computed as `SHA256(webhook_id + client_secret)`. It does not include a timestamp, so it provides no replay protection.

#### Verification example (Python)

```python theme={null}
import hashlib
import hmac

def verify_entri_signature_v1(
    webhook_id: str,
    client_secret: str,
    received_signature: str,
) -> bool:
    expected = hashlib.sha256(
        webhook_id.encode("utf-8") + client_secret.encode("utf-8")
    ).hexdigest()
    return hmac.compare_digest(expected, received_signature)
```
