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

# Receive webhooks

> Receive completed allocation terminations caused by a manual unsubscribe or an equity stop.

The MAM API sends a `mam.allocation.terminated` event when an allocation has finished terminating. This event covers both customer-initiated unsubscriptions and automatic exits caused by the configured equity stop.

Registering the webhook does not generate retroactive events. Only terminations completed after registration are delivered.

## Events you will receive

| `data.reason` | `data.triggered_by` | When it is sent |
| - | - | - |
| `USER_UNSUBSCRIBE` | `USER` | The user or CRM requested `POST /api/v1/mam/allocations/{allocation_id}/unsubscribe` and the allocation reached `CANCELLED`. |
| `EQUITY_STOP` | `SYSTEM` | The follower reached its configured equity stop and the automatic termination reached `CANCELLED`. |

> [!NOTE]
>
> For `CLOSE_ON_UNSUBSCRIBE`, the allocation can remain in `STOPPING` while copied positions are closed. The webhook is sent after termination completes, not when the process first starts.

## Register the destination

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "https://mam-api.example.com/api/v1/mam/webhooks" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "bridge-crm",
    "url": "https://crm.example.com/webhooks/mam"
  }'
```

The `201` response returns `signing_secret` in plain text only once. Store it immediately in a secrets manager.

## Callback request example

ExuraCopy sends an HTTP `POST` to the registered URL with signed headers and a JSON body:

```http theme={"theme":{"light":"github-light","dark":"github-dark"}}
POST /webhooks/mam HTTP/1.1
Content-Type: application/json
User-Agent: ExuraCopy-MAM-Webhook/1.0
X-MAM-Event-Id: 8b9f1de6-2d11-5465-acc6-6ec7f6a32a28
X-MAM-Event-Type: mam.allocation.terminated
X-MAM-Timestamp: 1785207545
X-MAM-Signature: sha256=<hmac_sha256_digest>
```

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "event_id": "8b9f1de6-2d11-5465-acc6-6ec7f6a32a28",
  "type": "mam.allocation.terminated",
  "version": 1,
  "occurred_at": "2026-07-28T00:19:05.020369Z",
  "data": {
    "allocation_id": 64,
    "reason": "USER_UNSUBSCRIBE",
    "triggered_by": "USER",
    "status": "CANCELLED",
    "leader_account_id": 31,
    "leader_login": "146493",
    "follower_account_id": 42,
    "follower_login": "146537",
    "unsubscribe_policy": "CLOSE_ON_UNSUBSCRIBE",
    "allocation_mode": "EQUITY",
    "mode_parameter": "1.00000000",
    "equity_limit": null,
    "stop_loss_value": null,
    "trigger_equity": null,
    "closed_positions": 0,
    "failed_positions": 0,
    "performance_fee_due": "0.00",
    "performance_fee_charged": "0E-8",
    "started_at": "2026-07-20T14:30:00Z",
    "terminated_at": "2026-07-28T00:19:05.020369Z"
  }
}
```

For an equity-stop termination, the relevant fields change as follows:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "reason": "EQUITY_STOP",
  "triggered_by": "SYSTEM",
  "stop_loss_value": "800.00000000",
  "trigger_equity": "798.42000000"
}
```

`stop_loss_value` is the configured threshold and `trigger_equity` is the equity observed when the stop was triggered. Either value can be `null` when the source could not provide it.

## Signed headers

| Header | Content |
| - | - |
| `X-MAM-Event-Id` | Stable, idempotent UUID. It matches the body `event_id`. |
| `X-MAM-Event-Type` | Always `mam.allocation.terminated` for this public contract. |
| `X-MAM-Timestamp` | Unix timestamp in seconds. |
| `X-MAM-Signature` | `sha256=<hexadecimal HMAC-SHA256 digest>`. |

## Validate the signature

Calculate the signature over the exact body bytes before deserializing JSON:

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
signed_payload = X-MAM-Timestamp + "." + raw_request_body
expected = "sha256=" + HMAC_SHA256(signing_secret, signed_payload).hexdigest()
```

Compare in constant time and reject timestamps with a difference greater than five minutes.

## Minimal callback handler

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import hashlib
import hmac
import json
import time

from fastapi import FastAPI, HTTPException, Request, Response

app = FastAPI()
MAM_SIGNING_SECRET = "read-from-your-secret-manager"


@app.post("/webhooks/mam")
async def receive_mam_event(request: Request):
    raw_body = await request.body()
    timestamp = request.headers.get("X-MAM-Timestamp", "")
    received_signature = request.headers.get("X-MAM-Signature", "")

    try:
        if abs(int(time.time()) - int(timestamp)) > 300:
            raise ValueError
    except ValueError:
        raise HTTPException(status_code=401, detail="Invalid webhook timestamp")

    signed_payload = timestamp.encode("ascii") + b"." + raw_body
    digest = hmac.new(
        MAM_SIGNING_SECRET.encode("utf-8"),
        signed_payload,
        hashlib.sha256,
    ).hexdigest()
    expected_signature = f"sha256={digest}"

    if not hmac.compare_digest(received_signature, expected_signature):
        raise HTTPException(status_code=401, detail="Invalid webhook signature")

    event = json.loads(raw_body)
    save_event_once(event["event_id"], event)  # UNIQUE event_id in your database
    return Response(status_code=204)
```

## Respond and implement idempotency

* Persist the event before responding.
* Enforce uniqueness on `event_id`.
* Respond quickly with any `2xx`; `204 No Content` is sufficient.
* A repeated delivery retains the same `event_id` and payload.
* Return `2xx` for an event that was already stored, without repeating side effects.

> [!WARNING]
>
> Receiving `mam.allocation.terminated` reports a termination that has already been processed. It does not initiate or authorize an unsubscription.


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