# Webhooks

A webhook pushes an event to your system when a lead receives their first message or replies. There is nothing to poll: accept the request, verify the signature and respond with 2xx.

## Webhooks or the event feed

Webhooks and the [event feed](https://docs.melioralab.io/en/events-polling/) carry the same facts: `lead.first_message_sent` and `lead.replied`. The difference is which side starts the exchange.

| | Webhooks | Feed `GET /v1/events` |
|---|---|---|
| How events arrive | Meliora sends a `POST` to your endpoint | Your system polls the API |
| What you need | A public `https` endpoint | An API key with `events:read` |
| Where it's configured | In the Meliora app | In your integration code |
| If your side is down | Retries for about 3 days, then a manual retry | Events wait in the feed for 30 days |
| Text of the first message | Not sent | `include_text=true` |
| `record_id` validation | None, the value arrives as uploaded | `record_id_status` |

Choose **webhooks** when you want to react quickly without constant polling and you have a server or an automation service with a public URL. Choose **the feed** when your system doesn't accept inbound requests, or when you must be able to collect every event after a long outage.

You can use **both**: webhooks as the fast signal, the feed as the safety net. A fact has the same `id` in a webhook and in the feed, so deduplicating by `id` prevents applying an event twice.

## How it works

1. Something happens in the app: a lead receives their first message, or a counted reply arrives.
2. Meliora collects the event and sends a `POST` with a JSON body to every matching endpoint.
3. Your system verifies the signature, stores the event and responds with a 2xx status within 10 seconds.
4. If there is no 2xx response, Meliora retries on a schedule with the same `id`.

An endpoint receives events collected after it was saved. Past events are not sent to a new endpoint: get the history from the [feed](https://docs.melioralab.io/en/events-polling/) or by listing [leads](https://docs.melioralab.io/en/reference/leads/).

## Set up an endpoint

Webhooks are managed by an **Owner or Admin** with access to every workspace and campaign in the organization. Other members can view the section but can't change anything. Webhooks don't need an API key.

1. Open **Settings › Integrations**, find the **Webhook events** block and click "Add endpoint".
2. Paste your system's URL into the endpoint URL field. The description is visible only in the app and helps you tell endpoints apart.
3. Under events, select "First message sent" (`lead.first_message_sent`), "Reply received" (`lead.replied`), or both.
4. Under campaigns, keep "All campaigns" to also receive events from campaigns created later, or pick specific ones. If your organization has several workspaces, choose workspaces the same way.
5. To receive the lead's reply text, turn on "Send reply text".
6. If the receiving side checks a key in a request header, add it under "Additional headers".
7. Save the endpoint and copy the signing secret from the box that appears.
8. Click "Test" and open "Log": the test event and your endpoint's response appear there.

You can view the signing secret again at any time with the "Signing secret" button on the endpoint card. An organization can have up to 10 endpoints. Webhooks are not available in the demo organization.

> **Note.** Webhooks are in early access. If the webhook block isn't in your settings, contact [Support](https://docs.melioralab.io/en/support/) and we will enable it for your organization.

### Endpoint requirements

- `https://` and port 443 only. A URL with another port can't be saved.
- A hostname, not an IP address.
- No username or password in the URL. For key-based access, use additional headers.
- A valid TLS certificate for the hostname from a public certificate authority. Self-signed certificates don't work.
- The hostname must resolve only to public internet addresses. If any resolved address points to a private network, delivery is refused.
- The hostname needs an A (IPv4) record: delivery over IPv6 only is currently not guaranteed.
- Redirects are not followed: a 3xx response counts as a failure. Use the final URL.
- Query parameters after `?` are kept and sent as is. The URL can be up to 2048 characters.

Requests come from Meliora servers. Their IP addresses are not published and can change, so don't filter requests by IP: verify the signature instead.

A URL can contain a secret path, so after you save it the app shows only its beginning and last characters. To change the URL, click "Edit" and paste the new one. Leaving the field empty keeps the current URL.

### Additional headers

If your service only accepts requests that carry a key in a header, add up to 5 headers. A name starts with `x-`, such as `x-api-key`, or is `Authorization`. A value can be up to 1024 characters. Values are hidden after you save them, and you can only replace them with "Replace headers". Signature headers and standard request headers can't be overridden.

## The request

Each event arrives as a separate `POST` request with a UTF-8 JSON body. The body is compact, without indentation, and non-ASCII characters are not escaped. The body is at most 256 KiB.

```http
POST /meliora/webhook HTTP/1.1
Host: crm.example.com
Content-Type: application/json
User-Agent: Meliora-Webhooks/1
webhook-id: evt_aab3db461d0542ff88c3124491aa2cc4
webhook-timestamp: 1793092509
webhook-signature: v1,kt8BoIJwYXnW8z5nUWwQitssRUMxKam8Dv+TgAhDV0s=

{"id":"evt_aab3db461d0542ff88c3124491aa2cc4","type":"lead.replied",...}
```

| Header | Value |
|---|---|
| `webhook-id` | The event ID: `evt_` followed by 32 hexadecimal characters. The same on every retry and equal to the `id` field in the body. |
| `webhook-timestamp` | When this attempt was sent, in Unix seconds. Each retry has its own value. |
| `webhook-signature` | The signature, `v1,<base64>`. During secret rotation, two signatures separated by a space. |
| `Content-Type` | `application/json` |
| `User-Agent` | `Meliora-Webhooks/1` |

Your additional headers are sent as well, if you set any. HTTP header names are case-insensitive, so read them without relying on case.

### The body

| Field | Description |
|---|---|
| `id` | The event ID and deduplication key. The same as the event's ID in the [feed](https://docs.melioralab.io/en/events-polling/). |
| `type` | `lead.first_message_sent`, `lead.replied`, or `webhook.test` for a test event. |
| `api_version` | The event format version, currently `2026-10-18`. See [API versions](https://docs.melioralab.io/en/versioning/). |
| `timestamp` | When the fact happened. This is not the send time: a retry carries the same `timestamp`. |
| `test` | `true` only for a test event. |
| `data` | The event data. |

The common `data` fields are described in the [events](https://docs.melioralab.io/en/reference/events/) reference: workspace, campaign, lead, sender account, first message time and reply. Examples and the differences between webhooks and the feed follow.

```json
{
  "id": "evt_61017785512246cdb0f3fafecba197a2",
  "type": "lead.first_message_sent",
  "api_version": "2026-10-18",
  "timestamp": "2026-10-27T09:02:11.204518+00:00",
  "test": false,
  "data": {
    "event_id": "evt_61017785512246cdb0f3fafecba197a2",
    "organization_id": "594b5bf4-aef7-480d-86b4-393390132326",
    "workspace": { "id": "3512de95-fcaa-4f57-b692-5caee3217b3e", "name": "Sales" },
    "campaign": { "id": "5cad4b4d-356f-4043-ba37-a42f51fc8a87", "name": "Autumn campaign" },
    "lead": {
      "id": "083f7a84-6cb7-49e1-9b1e-3a6ee783f70e",
      "username": "ivan_p",
      "username_at": "@ivan_p",
      "telegram_url": "https://t.me/ivan_p",
      "username_variants": [
        "ivan_p", "@ivan_p", "t.me/ivan_p", "https://t.me/ivan_p", "http://t.me/ivan_p",
        "https://t.me/ivan_p/", "telegram.me/ivan_p", "https://telegram.me/ivan_p"
      ],
      "telegram_user_id": 5012345678,
      "status": "contacted",
      "fields": { "record_id": "crm-10482", "first_name": "Ivan", "company": "Example Ltd" }
    },
    "account": { "id": "aaa3f0d2-63a1-44df-b091-410ebc606e8e", "username": "anna_sales", "name": "Anna" },
    "first_message_sent_at": "2026-10-27T09:02:11.204518+00:00"
  }
}
```
```json
{
  "id": "evt_aab3db461d0542ff88c3124491aa2cc4",
  "type": "lead.replied",
  "api_version": "2026-10-18",
  "timestamp": "2026-10-27T09:15:02.481233+00:00",
  "test": false,
  "data": {
    "event_id": "evt_aab3db461d0542ff88c3124491aa2cc4",
    "organization_id": "594b5bf4-aef7-480d-86b4-393390132326",
    "workspace": { "id": "3512de95-fcaa-4f57-b692-5caee3217b3e", "name": "Sales" },
    "campaign": { "id": "5cad4b4d-356f-4043-ba37-a42f51fc8a87", "name": "Autumn campaign" },
    "lead": {
      "id": "083f7a84-6cb7-49e1-9b1e-3a6ee783f70e",
      "username": "ivan_p",
      "username_at": "@ivan_p",
      "telegram_url": "https://t.me/ivan_p",
      "username_variants": [
        "ivan_p", "@ivan_p", "t.me/ivan_p", "https://t.me/ivan_p", "http://t.me/ivan_p",
        "https://t.me/ivan_p/", "telegram.me/ivan_p", "https://telegram.me/ivan_p"
      ],
      "telegram_user_id": 5012345678,
      "status": "replied",
      "fields": { "record_id": "crm-10482", "first_name": "Ivan", "company": "Example Ltd" }
    },
    "account": { "id": "aaa3f0d2-63a1-44df-b091-410ebc606e8e", "username": "anna_sales", "name": "Anna" },
    "first_message_sent_at": "2026-10-27T09:02:11.204518+00:00",
    "reply": {
      "id": "d3c448da-10fe-4493-ad22-55e1eb36478a",
      "replied_at": "2026-10-27T09:15:02.481233+00:00",
      "text": "Hi! Sounds interesting, tell me more"
    }
  }
}
```

Key order in the JSON is not guaranteed. Ignore unknown fields, and respond with 2xx to an event of an unknown type, otherwise it keeps being retried. See [API versions](https://docs.melioralab.io/en/versioning/#ustoychivyy-klient).

### Differences from the feed

| Field | Webhook | Feed `GET /v1/events` |
|---|---|---|
| `object` | Absent | `"event"` |
| `data.event_id` | Equal to `id` | Absent |
| `data.lead.username_at`, `telegram_url`, `telegram_user_id` | Present | Absent |
| `data.lead.username_variants` | Eight common ways to write the username | Absent |
| `data.lead.fields` | Every column uploaded with the lead except the phone number, as a snapshot at event time | Only the keys listed in [CRM mapping](https://docs.melioralab.io/en/crm-mapping/#polya), with `record_id` from the current lead record |
| `data.lead.record_id_status` | Absent | `valid`, `missing`, `conflict` or `unknown` |
| `data.lead.status` | The lead status when the event was collected | One of the reference values, otherwise `unknown` |
| `data.reply.text` | When "Send reply text" is on | With `include_text=true` |
| Text of the first message | Not sent | `data.first_message` with `include_text=true` |
| `data.reply.automation_stopped` | Absent | Always `null` |
| Time format | ISO 8601 UTC with a `+00:00` offset, up to microseconds | ISO 8601 UTC with `Z` and milliseconds |

What to know about webhook data:

- `data.lead.fields` holds the columns of the file uploaded during import or campaign creation. Names are lowercase Latin, and spaces become `_`: a `Lead Source` column arrives as `lead_source`. Phone numbers and internal fields are not sent.
- `data.lead.fields.record_id` is present if you uploaded such a column. Webhooks have no `record_id_status` check: if the value is missing or looks wrong, look the lead up with [`GET /v1/leads/{id}`](https://docs.melioralab.io/en/reference/leads/).
- `data.lead.status` shows the status at the moment the event was collected. Its set of values is not fixed: base decisions on `type`, not on this field.
- `data.lead.username` is lowercase, without `@`. If the lead has no username, `username` and the related fields are `null`.
- `data.account` is `null` if the sender account can't be found, for example because it was deleted.
- `data.first_message_sent_at` on a reply event comes from the lead record and can be `null`.

**Reply text.** Without "Send reply text", there is no `data.reply.text` field. With it, `text` holds the reply. If the text can't be read, `text` is `null` and `text_unavailable` is `true`.

**Size.** If the body exceeds 256 KiB, the reply text is removed first (`text` is `null`, `text_truncated` is `true`), then the lead fields (`fields` is empty, `fields_truncated` is `true`). If the body is still too large, the event is not sent and the log shows that the event is too large.

## Verify the signature

Anyone who learns your endpoint URL can send it a forged event. The signature proves that Meliora sent the request and that the body wasn't changed. Signatures follow the [Standard Webhooks](https://www.standardwebhooks.com/) specification, so its ready-made libraries work too.

A signing secret looks like `whsec_` followed by 44 base64 characters. Store it as a secret, for example in an environment variable, and never commit it to code.

To verify a request:

1. Take the **raw request body**: the bytes before any JSON parsing.
2. Build the signed content: the `webhook-id` value, a dot, the `webhook-timestamp` value, a dot, then the body.
3. The HMAC key is the part of the secret after `whsec_`, decoded from base64.
4. Compute HMAC-SHA256 over the signed content and encode the result in base64.
5. The `webhook-signature` header holds one or more space-separated signatures, each in the form `v1,<base64>`. The event is authentic if your result matches at least one `v1` signature. Compare in constant time, not with a plain `==`.
6. Check `webhook-timestamp`: the specification recommends rejecting requests whose time differs from yours by more than 5 minutes. This stops an intercepted request from being replayed later. Keep your server clock synchronized.

> **Warning.** Don't parse and re-serialize the JSON before verifying. A different key order, whitespace or character escaping produces different bytes, and the signature won't match. In your framework, read the body as bytes, for example with `express.raw()` instead of `express.json()`.

```js
import crypto from 'node:crypto';
import express from 'express';

const TOLERANCE_SECONDS = 5 * 60;
// The current secret; during rotation you can list two, separated by a comma
const SECRETS = process.env.MELIORA_WEBHOOK_SECRETS.split(',');

export function verifyWebhook(rawBody, headers, secrets) {
  const id = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  const signatures = headers['webhook-signature'];
  if (!id || !timestamp || !signatures) throw new Error('missing headers');

  const sentAt = Number(timestamp);
  if (!Number.isInteger(sentAt) || Math.abs(Date.now() / 1000 - sentAt) > TOLERANCE_SECONDS) {
    throw new Error('timestamp outside tolerance');
  }

  const signed = Buffer.concat([Buffer.from(`${id}.${timestamp}.`), rawBody]);
  const expected = secrets.map((secret) => {
    const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
    return Buffer.from(crypto.createHmac('sha256', key).update(signed).digest('base64'));
  });

  for (const part of signatures.split(' ')) {
    const [version, value = ''] = part.split(',');
    if (version !== 'v1') continue;
    const received = Buffer.from(value);
    if (expected.some((e) => e.length === received.length && crypto.timingSafeEqual(e, received))) {
      return JSON.parse(rawBody.toString('utf8'));
    }
  }
  throw new Error('no matching signature');
}

const app = express();

app.post('/meliora/webhook', express.raw({ type: 'application/json', limit: '1mb' }), async (req, res) => {
  let event;
  try {
    event = verifyWebhook(req.body, req.headers, SECRETS);
  } catch {
    return res.status(400).end();
  }
  await saveEvent(event); // insert with a unique event.id; a duplicate is simply skipped
  res.status(200).end();  // apply to the CRM after responding, in a separate job
});
```
```python
import base64
import hashlib
import hmac
import json
import os
import time

from fastapi import FastAPI, Request, Response

TOLERANCE_SECONDS = 5 * 60
# The current secret; during rotation you can list two, separated by a comma
SECRETS = os.environ["MELIORA_WEBHOOK_SECRETS"].split(",")

class InvalidWebhook(Exception):
    pass

def verify_webhook(raw_body: bytes, headers, secrets: list[str]) -> dict:
    msg_id = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp")
    signatures = headers.get("webhook-signature")
    if not msg_id or not timestamp or not signatures:
        raise InvalidWebhook("missing headers")
    try:
        sent_at = int(timestamp)
    except ValueError:
        raise InvalidWebhook("bad timestamp")
    if abs(time.time() - sent_at) > TOLERANCE_SECONDS:
        raise InvalidWebhook("timestamp outside tolerance")

    signed = f"{msg_id}.{timestamp}.".encode() + raw_body
    expected = []
    for secret in secrets:
        key = base64.b64decode(secret.removeprefix("whsec_"))
        digest = hmac.new(key, signed, hashlib.sha256).digest()
        expected.append(base64.b64encode(digest))

    for part in signatures.split(" "):
        version, _, value = part.partition(",")
        if version != "v1":
            continue
        if any(hmac.compare_digest(value.encode(), e) for e in expected):
            return json.loads(raw_body)
    raise InvalidWebhook("no matching signature")

app = FastAPI()

@app.post("/meliora/webhook")
async def meliora_webhook(request: Request):
    raw_body = await request.body()
    try:
        event = verify_webhook(raw_body, request.headers, SECRETS)
    except InvalidWebhook:
        return Response(status_code=400)
    save_event(event)  # insert with a unique event["id"]; a duplicate is simply skipped
    return Response(status_code=200)  # apply to the CRM after responding
```

Replace `saveEvent` and `save_event` with your own code: for example, an insert into a table with a unique key on `id`, plus a separate worker that applies events to your CRM.

### Test your implementation

Compute the signature for this data. Turn off the timestamp check for this example: the timestamp is in the past.

| | Value |
|---|---|
| Secret | `whsec_AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=` |
| `webhook-id` | `evt_aab3db461d0542ff88c3124491aa2cc4` |
| `webhook-timestamp` | `1793092509` |
| Body, no trailing newline | `{"id":"evt_aab3db461d0542ff88c3124491aa2cc4","type":"lead.replied","test":false}` |
| Expected signature | `v1,kt8BoIJwYXnW8z5nUWwQitssRUMxKam8Dv+TgAhDV0s=` |

The body in this example is shortened, which is enough to test the algorithm.

## Respond to the request

Respond with a **2xx status within 10 seconds**, and the event counts as delivered. No response body is needed, and Meliora doesn't store it.

Everything else counts as a failed attempt:

- 3xx, 4xx and 5xx statuses; redirects are not followed;
- no response within 10 seconds;
- the connection and TLS handshake not completing within 5 seconds;
- a DNS, connection or certificate error.

A single attempt is limited to 30 seconds in total. Respond as soon as the event is stored, and do CRM updates and other slow work after responding.

Special responses:

| Response | What Meliora does |
|---|---|
| `410 Gone` | Stops retrying this event and disables the endpoint right away. Return 410 only if the endpoint really is no longer needed. |
| `413 Payload Too Large` | Stops retrying this event. The endpoint keeps working. |
| `429` or `503` with `Retry-After` | The wait before the next attempt is the longer of the scheduled delay and `Retry-After` in seconds. Values above one hour count as one hour. An HTTP date in `Retry-After` isn't supported, and the schedule applies instead. |

### Retries

If an attempt fails, Meliora retries with the same `webhook-id` and the same body: up to 10 attempts over about 3 days. Each delay varies randomly by up to 10% either way.

| Attempt | Delay after the previous one | Approximate time since the first attempt |
|---|---|---|
| 1 | Right after the event is collected | |
| 2 | 5 seconds | 5 seconds |
| 3 | 5 minutes | 5 minutes |
| 4 | 30 minutes | 35 minutes |
| 5 | 2 hours | 2 h 35 min |
| 6 | 5 hours | 7 h 35 min |
| 7 | 10 hours | 17 h 35 min |
| 8 | 14 hours | 31 h 35 min |
| 9 | 20 hours | 51 h 35 min |
| 10 | 24 hours | 75 h 35 min |

After 10 failed attempts, the event is marked as not delivered. You can send it again from the endpoint's log: use "Retry" on the event, or "Retry failed in the last 7 days". A manual retry starts a new cycle of 10 attempts with the same `id`.

An event that isn't delivered within 7 days of being created, for example while the endpoint is paused, is canceled as not delivered within 7 days. You can retry it from the log too. The log keeps deliveries for 30 days.

### Automatic disabling

While an endpoint is failing, its card shows that there are errors. The endpoint is disabled automatically:

- right after a `410` response;
- if failures continue for more than 3 days and include at least 10 failed attempts in a row. Any 2xx response resets the count.

A disabled endpoint doesn't queue new events, and they won't be delivered. Fix the receiving side and click "Enable". Collect events from the downtime through the [feed](https://docs.melioralab.io/en/events-polling/) or by reconciling [leads](https://docs.melioralab.io/en/reference/leads/).

### Delivery guarantees

- **At least once.** The same event can arrive more than once, for example if your response was lost on the network. Deduplicate by `id` (the same as `webhook-id`) and answer a duplicate with 2xx.
- **No ordering guarantee.** `lead.replied` can arrive before `lead.first_message_sent`. Rely on `timestamp` and only move CRM statuses forward, as described in [CRM mapping](https://docs.melioralab.io/en/crm-mapping/#statusy-v-crm).
- **No delivery time guarantee.** Events are collected every few seconds and sent right after collection. Some events are found later, during reconciliation, and arrive late with an earlier `timestamp`.
- Currently, an endpoint receives at most one request at a time. Don't rely on this for ordering: retries are interleaved with new events.

## Rotate the signing secret

Rotate the secret if it may have leaked, or on your own schedule.

1. On the endpoint card, click "Signing secret", then "Rotate secret". Copy the new secret.
2. The previous secret stays valid for 24 hours. During that time, every request carries two signatures: one with the new secret and one with the previous secret. A receiver that still has the previous secret keeps working.
3. Replace the secret in your system within 24 hours. For a switch without a single error, you can accept both secrets during that time: the code above takes a list.

You can rotate again right away. The oldest secret then stops working immediately, the current one becomes the previous one, and the 24 hours start over. At most two secrets are valid at once.

## Test events

The "Test" button on an active endpoint's card sends a test event. You can press it at most once every 10 seconds. The result appears in the log.

A test event is signed and retried like a real one. The differences:

- `type` is `webhook.test` and `test` is `true`;
- `data` has the shape of `lead.replied`, and `data.example_of` is `"lead.replied"`;
- IDs in `data` are random, and the username is `example_lead`;
- `data.lead.fields` contains the column names of your recent leads with a placeholder value; real values are never sent;
- `data.reply.text` with sample text is present only when "Send reply text" is on.

A test event is handy for mapping fields, for example in an automation service. In production processing, skip events with `test: true`.

Sample values in a test event, such as workspace and campaign names, can be in Russian. Don't rely on them: they are placeholders.

## Pause, edit and delete

- **Pause.** The "Pause" button stops delivery. New events for the endpoint keep queuing and are sent after you click "Resume". An event that waits longer than 7 days is canceled, and you can retry it from the log.
- **Edit.** The "Edit" button changes the URL, events, scope, reply text and headers. If you narrow the events or the scope, queued events that no longer match are canceled.
- **Delete.** Click "Edit", then "Delete endpoint", and confirm. Queued events won't be sent, and the signing secrets are deleted.

## Delivery log

The endpoint card shows how many events were delivered and how many failed in the last 24 hours, how many are waiting, and how the last delivery ended. The log button opens the list of deliveries with their status (waiting, sending, delivered, not delivered or canceled), response code, attempt number and the time of the next attempt. A filter keeps only failed and waiting deliveries.

A 2xx status in the log means your endpoint accepted the request. What happened to the event after that is visible only in your own system's logs.

## Troubleshooting

| In the log | Cause and what to do |
|---|---|
| The URL isn't valid | Not `https`, a non-443 port, or an IP address instead of a hostname. Fix the URL. |
| The server wasn't found by name | The hostname doesn't resolve in DNS. Check your domain's records. |
| The URL points to an internal network | The hostname resolves to a private or reserved address. Use a public address. |
| The server refused the connection | Port 443 is closed or a firewall drops the requests. Open the port: Meliora server addresses aren't published, so you can't allowlist them. |
| A secure connection error | The certificate has expired, is self-signed, was issued for another name, or the chain is incomplete. |
| The server didn't respond in time | The response took longer than 10 seconds. Respond right after storing the event. |
| The server returned an error, code 3xx | A redirect, for example from `http` to `https` or to a URL with a trailing slash. Use the final URL. |
| The server returned an error, code 401 or 403 | Your service didn't accept the key. Check the additional headers. |
| The server returned an error, code 400 | Often this is your signature check. See below. |
| The event is too large | The body exceeds 256 KiB even without the reply text and lead fields. Contact [Support](https://docs.melioralab.io/en/support/). |

If the signature doesn't match, check in this order: the signature is computed over the raw body, not re-serialized JSON; you use this endpoint's secret and decode the whole part after `whsec_` from base64; nobody rotated the secret; your server clock is correct. Compare your code with the [test data](#test-vector).

If the log is empty although messages and replies are happening: the endpoint was created after those facts, the event type isn't selected, the campaign is outside the endpoint's scope, or the endpoint is paused or disabled.
