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

> ## Agent Instructions
> Melso product MCP: https://melso.ai/api/mcp (OAuth; one company per grant). REST: https://melso.ai. Documentation-search MCP: https://docs.melso.ai/mcp. The docs search server does not act on customer accounts.
>
> Use the live tool schema and respect company membership. Ask the person to authorize the exact fiscal document version in the signed-in app. A professional prepares and files included returns after authorization; AI does not file alone. A prepared file or approval receipt is not evidence of filing. Never claim completion from an accepted Task or Run. Keep credentials out of chat and logs. Agents features are internal or by private invitation.

# Webhooks

> Receive signed Task outcome notifications, verify delivery, and manage endpoint subscriptions.

<Info>Melso Agents is available only internally or by private invitation. Agents-specific features require existing access. For the autónomo fiscal service, see [Get started](/essentials/quickstart).</Info>

Subscribe to Task outcomes without remembering the latest Run ID. Subscribe to the event types you need.

| Event | Durable trigger | Extra summary fields |
| - | - | - |
| `task.delivered` | A Run delivers a substantive reply, attachment or branch and sets the open Task’s review marker; empty outcomes do not count. Registering a Task watch does not suppress a substantive delivery. | None |
| `task.needs_input` | A pending interaction is inserted. Emitted per interaction, including while its source Run is still active. | `interaction_id`, `interaction_kind` |
| `task.failed` | A Run becomes failed, or cancelled with `cloud_worker_unavailable` (the Task API’s Cloud failure classification), and commits without an automatic retry child. Nonretryable failures qualify; closed Tasks suppress this event. | `failure_reason` |
| `task.completed` | Task `completed_at` changes from null to set, including a GitHub close-intent merge or platform review completion. | None |
| `task.cancelled` | Task `cancelled_at` changes from null to set. Cancelling only a Run does not qualify. | None |

Reopening is intentionally not an event: it permits future work but is not a review/input/terminal outcome. Poll current Task details when you need all intermediate transitions. A delivery payload is a historical snapshot, not a promise that the Task remains in that state when your receiver processes it.

Webhooks notify external receivers. To resume a Melso-managed Task or chat agent after another accessible Task changes, use [`tasks_watch`](/concepts/tasks-api#persistent-task-watches). Task watches also support opt-in linked PR check events; these do not add event types to the webhook API.

## Payload

```json theme={null}
{"id":"event-uuid","type":"task.delivered","created_at":"2026-09-23T12:00:00Z","workspace_id":"workspace-uuid","task_id":"task-uuid","task_identifier":"MELS-115","run_id":"run-uuid","task_status":"to_review","run_status":"completed"}
```

`id`, `workspace_id`, `task_id` are UUID strings; `type`, `task_identifier`, `task_status` are strings; `created_at` is a timestamp. `run_id` and `run_status` can be null when a Task closes without a Run. `task_status` follows normal Task precedence, so a pending ask can carry `working` while its Run exits. Payloads omit prompt text, answers and secrets; fetch details using the Task and interaction APIs.

## Signing and delivery

Each POST carries `Content-Type: application/json`, `Melso-Event-ID`, and `Melso-Signature: t=UNIX_SECONDS,v1=HEX_DIGEST`. Compute HMAC-SHA256 using the **literal secret string** over `timestamp + "." + raw_request_body`. Compare signatures in constant time, enforce a timestamp tolerance (for example five minutes), then de-duplicate by event `id`. Do not parse/re-serialize JSON before verification. Every delivery attempt, including an automatic retry or manual resend, is signed using its attempt timestamp and the current secret. Event IDs and payloads stay fixed. Attempts in the same second can have the same signature when the secret is unchanged.

The company secret is shared by registered endpoints and per-task callbacks. First endpoint creation initializes it and reveals it once. Subsequent creates omit `signing_secret`. Rotation returns a new secret once and immediately replaces the key for future attempts, including retries; an already in-flight request can still use the old key. Coordinate receiver rotation accordingly.

State changes write payloads and recipients transactionally to an outbox; a separate worker POSTs them. A `2xx` acknowledges delivery. Every other status, redirect, network error or timeout retries with exponential backoff (30 seconds, doubling to one hour, plus 0–25% jitter) until the 24-hour window expires. Workers recover expired one-minute leases. Delivery is at least once within this window, with possible duplicates and no ordering guarantee; permanently unavailable receivers end in `failed`. Resend opens another 24-hour window with the same event ID and cumulative attempt count.

HTTPS is required in all environments. Registration and the connection dialer reject private, loopback, link-local, metadata and reserved addresses, including mixed public/private DNS answers. Delivery pins the validated address, disables proxies/redirects and limits requests to 15 seconds. The log stores at most 4 KiB of UTF-8 response text. Removing/disabling an endpoint suppresses queued future attempts, but cannot recall an in-flight request. Endpoint edits affect future events; queued deliveries retain their original URL. Task callbacks receive all five events and are independent of endpoint subscriptions.

Webhook management routes require a **human company owner/admin** using a personal token or browser session. Company keys with either `tasks` or `admin` scope, Run tokens and MCP actors are denied; see the [credential matrix](/api-reference/authentication#webhook-management-credentials). Recent deliveries return at most 100 rows, newest first. The log keeps cumulative attempts and the most recent response/status, not a separate row for each network attempt.

## Connect a receiver

Use an HTTPS receiver you control. Register its public URL through the [Webhooks reference](/api-reference/tags/webhooks), store the returned signing secret securely, and verify requests with the inline functions below. Persist the event and enqueue its work before acknowledging delivery.

## Verify a signature

Pass the exact raw body bytes, the `Melso-Signature` header and the literal signing secret. The functions accept a five-minute clock tolerance and reject timestamps too far in the past or future. Keep your receiver clock synchronized.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { createHmac, timingSafeEqual } from 'node:crypto';

  /** Pass the original HTTP body, before JSON parsing, and the literal secret. */
  export function verifyMelsoWebhook(
    raw: Buffer,
    signature: string,
    secret: string,
    now = Math.floor(Date.now() / 1000),
    tolerance = 300,
  ): boolean {
    if (!secret) return false;
    const timestamps: string[] = [];
    const signatures: string[] = [];
    for (const part of signature.split(',')) {
      const match = /^([^=]+)=(.*)$/.exec(part.trim());
      if (!match) return false;
      if (match[1] === 't') timestamps.push(match[2]);
      if (match[1] === 'v1') signatures.push(match[2]);
    }
    if (timestamps.length !== 1 || !/^\d+$/.test(timestamps[0])) return false;
    const timestamp = Number(timestamps[0]);
    if (!Number.isSafeInteger(timestamp) || Math.abs(now - timestamp) > tolerance) return false;
    const expected = createHmac('sha256', secret).update(timestamps[0] + '.').update(raw).digest();
    return signatures.some(value => /^[a-f0-9]{64}$/i.test(value) && timingSafeEqual(expected, Buffer.from(value, 'hex')));
  }
  ```

  ```python Python theme={null}
  import hashlib, hmac, re, time

  def verify(raw:bytes,signature:str,secret:str,now:int|None=None,tolerance:int=300)->bool:
      if not secret:return False
      parts=signature.split(',');timestamps=[];digests=[]
      for part in parts:
          key,sep,value=part.strip().partition('=')
          if not sep:return False
          if key=='t':timestamps.append(value)
          elif key=='v1':digests.append(value)
      if len(timestamps)!=1 or not re.fullmatch(r'[0-9]{1,20}',timestamps[0]):return False
      stamp=timestamps[0];now=int(time.time()) if now is None else now
      if abs(now-int(stamp))>tolerance:return False
      expected=hmac.new(secret.encode(),stamp.encode()+b'.'+raw,hashlib.sha256).hexdigest()
      return any(re.fullmatch(r'[a-fA-F0-9]{64}',d) and hmac.compare_digest(expected,d.lower()) for d in digests)
  ```
</CodeGroup>

### Deterministic test vector

Use this public test secret only for the example: `whsec_documentation_test_only`. The exact UTF-8 body below has **no trailing newline**. It is a minimal signature fixture, not a complete event payload.

```json theme={null}
{"id":"11111111-1111-4111-8111-111111111111","type":"task.delivered"}
```

For timestamp `1800000000`, the expected `Melso-Signature` header is:

```text theme={null}
t=1800000000,v1=d7dffdbfbd0b3a34eeed08ae82ad32776a90ed6587d92e421637c1186822f534
```

Append the matching test to the verifier above. It fixes `now` to the signed timestamp so this test remains repeatable. Production receivers must use the actual current time.

<CodeGroup>
  ```typescript TypeScript test theme={null}
  const raw = Buffer.from("{\"id\":\"11111111-1111-4111-8111-111111111111\",\"type\":\"task.delivered\"}", 'utf8');
  const secret = "whsec_documentation_test_only";
  const signature = "t=1800000000,v1=d7dffdbfbd0b3a34eeed08ae82ad32776a90ed6587d92e421637c1186822f534";
  if (!verifyMelsoWebhook(raw, signature, secret, 1800000000)) throw new Error('Valid vector rejected');
  if (verifyMelsoWebhook(Buffer.concat([raw, Buffer.from('\n')]), signature, secret, 1800000000)) throw new Error('Changed body accepted');
  if (verifyMelsoWebhook(raw, signature, secret, 1800000301)) throw new Error('Expired vector accepted');
  console.log('Signature vector passed');
  ```

  ```python Python test theme={null}
  raw = b'{"id":"11111111-1111-4111-8111-111111111111","type":"task.delivered"}'
  secret = 'whsec_documentation_test_only'
  signature = 't=1800000000,v1=d7dffdbfbd0b3a34eeed08ae82ad32776a90ed6587d92e421637c1186822f534'
  assert verify(raw, signature, secret, now=1800000000)
  assert not verify(raw + b'\n', signature, secret, now=1800000000)
  assert not verify(raw, signature, secret, now=1800000301)
  print('Signature vector passed')
  ```
</CodeGroup>

Both tests print `Signature vector passed`. After verification in your receiver, parse the payload and persist its event ID with the work you need to do before acknowledging. Use a durable inbox with a unique event ID so repeated deliveries do not repeat your business action. A separate worker can process saved events and record completion.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.