# Event delivery to your agent

> Receive signed operational events at an endpoint you own.

Canonical page: https://anectico.com/docs/agents/event-delivery/


An idle MCP connection cannot receive a push. Give Anectico a public HTTPS endpoint you own.
Anectico sends a signed JSON event there when a subscribed change happens. Your receiver can
wake your own agent. Anectico does not investigate, decide, fix anything, or run your agent.

A destination belongs to one project and the API key that creates it. Use a management key
created by a person in **Agents and keys**, scoped to that project. An intake key, application key, key created by
another key, or OAuth connection cannot create a destination. A key sees only its own
destinations. A workspace owner or admin can also inspect, pause, or delete them in the Console.
The receiver URL cannot be changed. Create a new destination to move to another endpoint.

## Events and schemas

Every document has `version: 1`, `id`, `type`, `occurred_at`, `delivered_at`, `attempt`,
`workspace_id`, `project_id`, `subject`, and `facts`. The [JSON Schema](https://anectico.com/schemas/agent-events-v1.json)
contains a separate definition for every type. Unknown fields are forbidden. IDs are UUIDs.
Times are UTC. `occurred_at` is the original change time. `delivered_at` and `attempt` change
on a resend. `attempt` is the reserved attempt number, from 1 to 8.

| Type and schema definition | The same occurrence means | Subject and facts | Read scope |
| --- | --- | --- | --- |
| `issue.first_seen` (`$defs/issue.first_seen`) | One issue and its original first-seen time | `issue`; state `new` | `errors:read` |
| `issue.regressed` (`$defs/issue.regressed`) | One issue and its regression time | `issue`; state `regressed` | `errors:read` |
| `alert.fired` (`$defs/alert.fired`) | One alert and its original trigger time | `alert`; state `firing`; optional severity `info`, `warning`, or `critical` | `alerts:read` |
| `alert.recovered` (`$defs/alert.recovered`) | One alert and its measured recovery time | `alert`; state `recovered` | `alerts:read` |
| `incident.opened` (`$defs/incident.opened`) | One incident and its creation time | `incident`; state `open`; optional severity `sev1` to `sev4` | `incidents:read` |
| `incident.resolved` (`$defs/incident.resolved`) | One incident and its resolution time | `incident`; state `resolved` | `incidents:read` |
| `test` (`$defs/test`) | One explicit test request; a new ID for each request | `destination`; state `test` | `event_destinations:write` |

Subscribe to `issue.first_seen`, `issue.regressed`, `alert.fired`, or `incident.opened`.
Set `include_recovery: true` on an alert or incident subscription to also receive its recovery
or resolution. Recovery is off by default. Manually closing an alert is not a measured
recovery. Waking a snoozed issue is not a regression. There are no separate release or metric
watch event types yet. Workspace-wide notifications have no project destination and are not sent.

For operational events, the ID is deterministic. Join these lines with `\n`, with no final newline:
`anectico.agent-event.v1`, type, workspace ID, project ID, subject ID, original UTC transition
time in RFC3339Nano format. Hash the UUID URL namespace bytes
(`6ba7b811-9dad-11d1-80b4-00c04fd430c8`) followed by those UTF-8 bytes using SHA-256.
Take the first 16 bytes, set the UUID version nibble to 5 and RFC variant bits, and format as a
UUID. This is Anectico's specified SHA-256 derivation; it does not use the usual UUIDv5 SHA-1
algorithm. Re-evaluating or re-delivering the same change keeps this ID.

Documents contain closed state and severity values, not error messages, log lines, user-written
titles, labels, person IDs, emails, or event properties. They do not contain measured counts.
Missing severity is unknown. A proof URL is present only for an issue or incident with a proof
page. Your agent reads details with its own key and permissions through MCP or the CLI.
Destination documents never go to your ordinary notification channels. Your ordinary workspace
notifications never go to an event destination.

## Set up a destination

The key needs `event_destinations:write`, `event_destinations:read`, and the read scope for each
subscribed subject. MCP writes also need `mcp:write`; MCP reads need `mcp:read`.
The recorded scope ceiling is the key’s concrete scopes at creation. Granting a new read
scope later does not widen an old destination. Create a new destination if the subscription
needs a scope outside that ceiling. Removing a scope still stops existing subscriptions.

Event delivery and the current subject reads are available on every plan. Creating or responding
to an incident still follows its own plan rules.

Create a file containing the receiver and subscriptions:

```json
{
  "url": "https://receiver.example.test/anectico",
  "subscriptions": [{"type": "alert.fired", "include_recovery": true}]
}
```

Use `anectico event-destinations create --file destination.json`. The active project supplies
the project ID. The response contains the new destination and a signing secret shown once.
Save it securely in your receiver. Get, list, receipts, and later reads never return that secret.
If the create response is lost, inspect the list before retrying: creating again makes another
destination. Rotate the secret if you could not save it.

Through MCP, discover `create_agent_destination` with `list_write_actions`, then use
`execute_external_action` with `action: "create_agent_destination"`. The first call previews the
owner key, project, exact URL, subscriptions, and active state without creating anything.
Approve those fields using the returned confirmation token. Only the confirmed response shows
the new secret once. Do not include it in notes, audit text, or a conversation transcript.

## Verify the signature

Headers are `Anectico-Event-ID`, `Anectico-Timestamp`, and `Anectico-Signature`.
The signature header has the form `t=1791536400,v1=<lowercase hexadecimal digest>`.
The signed bytes are the decimal Unix timestamp, one ASCII period, then the **exact raw request
body bytes**. Compute HMAC-SHA-256 using the signing secret's literal UTF-8 bytes. Do not decode
the secret as base64 and do not parse or reserialize the JSON before checking it.

Reject timestamps more than 300 seconds from your receiver's clock. Compare signatures in
constant time. Then parse the validated document, require version 1, check the schema and that
the body ID matches the header ID, and deduplicate by event ID. Save the ID before starting
your agent. A timestamp tolerance alone does not remove duplicates inside that window.

This Python example checks a request using the current secret and, during rotation, the
previous one. Both are obtained from your receiver's secure configuration:

```python
import hashlib
import hmac
import json
import time

def verify(raw_body, signature_header, timestamp_header, event_id_header, secrets):
    if len(raw_body) > 8192:
        raise ValueError("body too large")
    fields = [part.split("=", 1) for part in signature_header.split(",")]
    timestamps = [value for name, value in fields if name == "t"]
    signatures = [value for name, value in fields if name == "v1"]
    if len(timestamps) != 1 or timestamps[0] != timestamp_header:
        raise ValueError("timestamp mismatch")
    timestamp = timestamps[0]
    if not timestamp.isascii() or not timestamp.isdecimal():
        raise ValueError("invalid timestamp")
    if abs(time.time() - int(timestamp)) > 300:
        raise ValueError("expired signature")
    signed = timestamp.encode("ascii") + b"." + raw_body
    valid = False
    for secret in secrets:
        expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
        for signature in signatures:
            valid |= hmac.compare_digest(expected, signature)
    if not valid:
        raise ValueError("invalid signature")
    event = json.loads(raw_body)
    if event["id"] != event_id_header or event["version"] != 1:
        raise ValueError("event mismatch")
    return event  # Validate against the published schema before accepting it.
```

Public test vector: secret `example-only`; timestamp `1791536400`; body bytes `{"id":"test"}`
with no newline. The digest is
`03ba7b3f1fdfcd42bf500da12b56741339d5c0cf355a394cff885e2517b8a255`.
The small body demonstrates signing only; it is not a complete event document.

Use `anectico event-destinations rotate-secret <id> --yes` or the confirmed MCP action
`rotate_agent_destination_secret`. The new secret is shown once. For one hour Anectico sends
two `v1` values: one with the new secret and one with the previous secret. A receiver accepts
either. Install the new secret before removing the old one. Another rotation is refused while
the overlap is active.

## Retries, stops, and limits

Delivery is at least once. A lost response can cause the same event to arrive again.
Ordering is not guaranteed. Reply with any 2xx status to accept a request. Redirects are
failures and are never followed. Response bodies are not kept.

A send times out after 10 seconds. Each attempt has a 20-second work budget. DNS validation
also happens on each attempt and the connection pins the validated address. Private, loopback,
link-local, and other forbidden addresses are refused. A destination reserves at most one send
per second. Bodies are at most 8 KiB. A key can own 5 destinations and a project can have 20.
There are at most 4 base subscriptions per destination. Attempt reads default to 50 records,
allow at most 100 per page, and refuse cursor offsets above 10,000. The final allowed page
can contain records 10,001 to 10,100. This is a navigation bound, not a total. The worker claims at
most six deliveries per pass, with at most three due rows per workspace. Both event
and ordinary-notification retries share eight concurrent sends per process, with
at most one in flight per workspace. A slow receiver cannot fill all eight places.
These limits are bounds, not measured activity totals.

Events have no delivery-order guarantee. Independent workspaces run in parallel;
sends to a workspace are sequential within each process. Different replicas still
serialize sends to a destination. Receivers must deduplicate event IDs and use the
event timestamps when order matters.

Transport and HTTP failures retry with exponential backoff: a 30-second starting delay,
doubled per failed attempt up to one hour, then jittered to 50–100% of that delay. There are
at most 8 reserved attempts. A document older than 24 hours from `occurred_at` is dead-lettered.
Five consecutive failed sends disable the destination. A successful send resets that count.
An address refusal is terminal. The owner can read the reason and explicitly resume a disabled
destination, which resets the failure count.

Before every attempt Anectico checks the project, the current owner key, its recorded scope
ceiling, and the required subject read scope. Revoked, frozen, expired, suspended, or contained
keys stop deliveries. A deleted project, removed scope or subscription, paused destination,
or unavailable signing authority stops that attempt with a recorded reason. Such stops are
terminal, not transport retries. Making the key valid or resuming the destination does not
replay stopped events. A queued event that has not reached a stop may still send under the
normal retry rules while it is less than 24 hours old. An already in-flight request cannot
be recalled; freezes prevent further attempts after the next authority check.

Use `anectico event-destinations attempts <id>` or `list_agent_delivery_attempts` through
`execute_read_action` to read recorded times, outcome classes, HTTP status when a response
was received, duration, and reasons. `unknown` means an attempt was reserved but its outcome
was not recorded, for example after a process interruption. It is neither success nor failure.
The result describes the page's population and time window. It does not claim a total.

## Test and manage

Use `anectico event-destinations test <id> --yes` or the confirmed `test_agent_destination`
action to queue a signed `type: "test"` document. It has its own event ID, goes through the
same checks and retry path, and does not indicate an operational problem. Read its attempts
to confirm acceptance; a queue receipt alone does not prove delivery.

`anectico event-destinations list`, `get <id>`, and `update <id> --file subscriptions.json`
inspect or replace subscriptions. `pause <id> --yes`, `resume <id> --yes`, and
`delete <id> --yes` control delivery. Deleting a destination removes its queued deliveries
and attempt history. Project or workspace deletion removes these records too. Event documents
carry no tracked-person data, so there is no person archive or erasure entry for them.

The read actions are `list_agent_destinations`, `get_agent_destination`, and
`list_agent_delivery_attempts` through `execute_read_action`. The other confirmed write actions
are `update_agent_destination`, `pause_agent_destination`, `resume_agent_destination`, and
`delete_agent_destination`; discover their correct write gateway with `list_write_actions`.

## Ask your agent

Use `create_agent_destination` through `execute_external_action`, then
`list_agent_delivery_attempts` through `execute_read_action`. The CLI path is
`anectico event-destinations create --file destination.json` and
`anectico event-destinations attempts <id>`.

“When an alert fires, deliver it to my endpoint at this URL so my system can wake you. Include
recoveries. Show me the project, owner key and subscriptions before creating it. After I approve,
help me store the signing secret securely and send a test event. Check the recorded outcome.”

“Pause this key's event destination, and show me why its last attempt stopped.”

Use the `anectico-alerts-setup` skill for this optional setup step. Deliver to an endpoint only
when the person asks for it. The foundation `anectico-cli` skill helps find this capability.
