> ## 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

<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](/domain-purchasing). 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"`).

#### Event type

* **`type`**: Defines the type of event. Possible values:
  * `"domain.added"`:   Indicates that DNS records have been successfully added.
    * For **Entri Sell**, this means the domain has been registered and connected.
    * For **Entri Connect**, it confirms that DNS records have been added to an existing domain.
    * Multiple messages of this type may be received for the same domain. This occurs when either the propagation\_status or redirection\_status changes.
  * `"domain.purchased"`: Indicates that the domain *order* creation has been completed.
  * `"domain.propagation.timeout"`: This event is very rare and is triggered if DNS records fail to propagate after 72 hours. It is highly uncommon for domains connected through the automatic flow.
  * `"domain.flow.completed"`: Indicates that the user has completed the Entri domain connection flow. This event does not convey any propagation status — it only confirms that the user finished the flow steps.
  * `"domain.record_missing"`: Only applicable when using **Entri Monitor**. A DNS record that was previously detected is no longer found. This may indicate it was removed or is temporarily unavailable.
  * `"domain.record_restored"`: Only applicable when using **Entri Monitor**. A previously missing DNS record has been detected again.
  * `"domain.transfer.in.initiated"`: Only applicable for **Entri Sell Enterprise**. An inbound domain transfer has been initiated by the user.
  * `"domain.transfer.in.started"`: Only applicable for **Entri Sell Enterprise**. The inbound domain transfer has been accepted and is now in progress at the registry level.
  * `"domain.transfer.in.ack"`: Only applicable for **Entri Sell Enterprise**. The inbound domain transfer has been acknowledged by the registry.
  * `"domain.transfer.in.failed"`: Only applicable for **Entri Sell Enterprise**. The inbound domain transfer has failed.
  * `"domain.transfer.out.initiated"`: Only applicable for **Entri Sell Enterprise**. An outbound domain transfer away from Entri has been initiated.
  * `"purchase.error"`: Indicates an error during the domain purchase process.
  * `"purchase.confirmation.expired"`: The purchase confirmation has expired (abandoned flow).

#### Status fields

* **`propagation_status`**: The status of DNS record propagation. Possible values:
  * `"pending"`: DNS records are still being propagated.
  * `"success"`: DNS records have been successfully propagated.
  * `"failed"`: DNS record propagation has failed. This is triggered if the records are not propagated even after 72 hours.
  * `"exempt"`: Only present in notifications with `type:"purchase.confirmation.expired"`.

* **`dkim_status`**: The status of DKIM (DomainKeys Identified Mail) configuration. Possible values:
  * `"pending"`: DKIM setup is pending.
  * `"success"`: DKIM configuration has been successfully completed.
  * `"failed"`: DKIM setup failed.
  * `"exempt"`: DKIM setup is exempt, usually if the user does not use a supported email provider (e.g. Google or Microsoft).

* **`redirection_status`**: The status of domain redirection. Possible values:
  * `"pending"`: Redirection setup is in progress.
  * `"success"`: Redirection setup has completed successfully.
  * `"failed"`: Redirection setup failed.
  * `"exempt"`: Redirection is exempt from this setup.

* **`setup_type`**: Indicates whether the setup was done automatically or manually. Possible values:
  * `"automatic"`: Setup was performed automatically.
  * `"manual"`: Setup required manual intervention.
  * `"async"`: Setup was done using the [DNS asynchronous update](/api-reference#asynchronous-dns-configurations-entri-sell-only).
  * `"api"`: The setup was performed using the [**Entri Sell Enterprise Api**](/entri-sell-enterprise-api)

#### Advanced DMARC configuration feature

This field is present in the webhook when using the [DMARC Handling: Advanced Options](/advanced-dmarc-options) or `validateDmarc` features.

* **`dmarc_updated`**: Reflects the status of DMARC (Domain-based Message Authentication, Reporting, and Conformance) updates. Possible values:
  * `"true"`: DMARC settings have been updated.
  * `"false"`: DMARC settings were not updated.
  * `"exempt"`: DMARC is not applicable in this situation (e.g. for domains that don’t require it).

#### Entri Secure status

* **`secure_status`**: Only relevant when using Entri Secure. Describes the status of SSL security configuration. Possible values:
  * `"pending"`: SSL configuration is pending.
  * `"success"`: SSL setup was successful.
  * `"failed"`: SSL setup failed.
  * `"exempt"`: Entri Secure does not apply to the current flow.

#### Entri Power status

* **`power_status`**: Only relevant when using Entri Power. Describes the status of the advanced features configuration. Possible values:
  * `"pending"`: Setup of advanced features is pending.
  * `"success"`: Advanced features setup was successful.
  * `"failed"`: Setup of advanced features failed.
  * `"exempt"`: Entri Power does not apply to the current flow.

#### Entri Monitor status

* **`monitor_status`**: Only relevant when using Entri Monitor. Describes the status of the monitoring feature. This key will not be shown on flows that don't use Entri Monitor. Possible values:
  * `"pending"`: Monitoring domain setup is pending.
  * `"success"`:  Monitoring domain setup was successful.
  * `"failed"`:  Monitoring domain setup failed.

#### CNAME and domain information

* **`cname_target`**: Only applicable for Entri Power and Secure setups. The CNAME target used for DNS configuration (e.g. `"my.saascompany.com"`).

* **`purchased_domains`**: An array of domains that were purchased during the transaction. This field is only relevant for Entri Sell. Example:

  ```json theme={null}
  ["domain1.com", "domain2.com", "domainN.com"]
  ```

* **`free_domain`**: Indicates whether the domain was obtained through a free domain flow. Possible values:
  * `true`: The domain was free.
  * `false`: The domain was not free.

#### Data object (DNS records)

The `data` object contains the DNS records that have been propagated and those that are pending.

* **`records_propagated`**: An array of DNS records that have been successfully propagated. Each record contains:
  * **`type`**: The type of DNS record (e.g. `"A"`, `"CNAME"`).
  * **`host`**: The hostname for which the DNS record applies (e.g. `"smallbusiness.com"`).
  * **`value`**: The value of the DNS record (e.g. `"54.153.2.220"`).
  * **`applicationUrl`**: (optional, Entri Power only) The application URL tied to the propagated DNS record (e.g. `"www.example.com"`).
  * **`powerRootPathAccess`**: (optional, Entri Power only) An array of paths accessible through Entri Power. Example:

    ```json theme={null}
    ["path1", "path2", "pathN"]
    ```

* **`records_non_propagated`**: An array of DNS records that have not yet propagated. The structure is the same as `records_propagated`.

#### Shared flows and sharing link domains

When a user completes an Entri flow that was initiated via a [sharing link](/api-reference#sharing-links-api), Entri fires webhook events just like any standard flow. A few things to keep in mind:

* The `job_id` returned when the sharing link was created is the identifier used to correlate all webhook events for that flow. It remains consistent regardless of whether a [custom sharing link domain](/api-reference#sharing-link-custom-domain) is configured — the domain only affects the URL of the link itself, not the webhook payload.
* To retrieve the last webhook event for a shared flow, use the [Get last webhook by job ID](/api-reference#get-last-webhook-by-job-id) endpoint with the `job_id`.
* If the flow was initiated with a `sharedFlowId`, webhook events for the derived flow will include an `originFlowId` field. See [Correlating Shared / Derived Flows](/api-reference#correlating-shared-derived-flows) for details.

### 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`.

# 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, and is required for Signature V2 verification. This enables servers to enforce freshness windows and reject replayed requests.

## Signature Verification Methods

### Signature V2 (Recommended)

Entri now uses **`Entri-Signature-V2`** together with **`Entri-Timestamp`** for stronger authenticity and replay protection. The V2 signature is an **HMAC-SHA256** computed with your client secret over a canonical message that includes both the webhook id and the timestamp. This ensures each request is unique and time-bound, helping prevent replay attacks and providing a stronger guarantee of authenticity.

#### How It Works

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

#### Required Components

1. Webhook payload’s `id` value
2. Client secret from your Entri dashboard
3. `Entri-Timestamp` header — Unix timestamp used for Signature V2 verification
4. `Entri-Signature-V2` header

#### Signature V2 Verification Example (Python)

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

def validate_entri_signature_v2(
    webhook_id: str,
    client_secret: str,
    received_signature: str,
    received_timestamp: str,
    max_age_seconds: int = 300
) -> bool:
    """
    Validate the Entri-Signature-V2 webhook signature.
    
    Args:
        webhook_id: The webhook ID from request body["id"]
        client_secret: Your client secret
        received_signature: The 'Entri-Signature-V2' header value
        received_timestamp: The 'Entri-Timestamp' header value
        max_age_seconds: Maximum allowed age of request (default: 5 minutes)
    
    Returns:
        True if signature is valid and fresh, False otherwise
    """
    try:
        # Verify timestamp freshness (prevent replay attacks)
        current_time = int(time.time())
        request_time = int(received_timestamp)
        if current_time - request_time > max_age_seconds:
            return False
        
        # Recreate the signature
        expected_signature = hashlib.sha256(
            webhook_id.encode() + received_timestamp.encode() + client_secret.encode()
        ).hexdigest()
        
        # Constant-time comparison
        return hmac.compare_digest(expected_signature, received_signature)
        
    except (ValueError, TypeError, AttributeError):
        return False
```

### Signature V1 (Legacy)

For legacy integrations, Entri previously used a different signature method. In this approach:

1. The entire webhook payload (as a JSON string) is concatenated with your client secret.
2. A SHA-256 hash of this string is computed.
3. The resulting hash is sent in the `Entri-Signature` header.

#### Signature V1 Verification Example

```python theme={null}
import hashlib

def verify_webhook_signature(webhook_id: str, secret_token: str, received_signature: str) -> bool:
    """
    Verify the authenticity of an Entri webhook request.

    Args:
        webhook_id (str): The Id value from the webhook payload
        secret_token (str): Your secret token from the Entri dashboard
        received_signature (str): The value from the Entri-Signature header

    Returns:
        bool: True if signature is valid, False otherwise
    """
    hash_obj = hashlib.sha256(webhook_id.encode('utf-8'))
    hash_obj.update(secret_token.encode('utf-8'))
    calculated_signature = hash_obj.hexdigest()

    return calculated_signature == received_signature
```
