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

# Signed CRM webhooks

> Register a receiver, rotate its secret and process retried events exactly once in the CRM.

The broker provides a **public HTTPS receiver**. MAMX calls it; the CRM does not poll a webhook endpoint for new events. Registration and updates use the broker API key.

| Action | Request |
| - | - |
| Register | `POST /api/v1/mamx/webhooks` with `{"name":"primary","url":"https://crm.example.com/mamx"}` |
| Change URL only | `PATCH /api/v1/mamx/webhooks` with `{"url":"https://crm.example.com/new"}` |
| Generate a new key | Same PATCH with `{"rotate_secret":true}` |
| Supply a key | Set `signing_secret` in POST or PATCH (32–1,024 bytes); do not also request rotation. |

One destination per configured MT5 server. Registration returns 201; an existing destination returns 409. Store the generated secret when returned: it cannot be retrieved later. Supplied secrets are not echoed. An update affects the next attempt without a restart; an in-flight delivery may still use the old URL and key. Coordinate overlap with the receiver. Redirects are not followed.

## Verify before parsing

The sender signs **the exact bytes** of the HTTP body. Parsing and re-encoding JSON first can break the signature. Example for trusted server-side Python code:

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

def verified_event(raw_body, headers, secret):
    stamp = headers["X-MAMX-Timestamp"]
    if not stamp.isascii() or not stamp.isdecimal() or abs(time.time() - int(stamp)) > 300:
        raise ValueError("Invalid or stale timestamp")
    signed = stamp.encode("ascii") + b"." + raw_body
    digest = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
    if not hmac.compare_digest("sha256=" + digest, headers["X-MAMX-Signature"]):
        raise ValueError("Invalid signature")
    return json.loads(raw_body)
```

Persist `event_id` with a unique constraint **in the same CRM transaction** as the state update. Return 2xx only after commit (also for an already processed ID). Delivery is at least once, not ordered. Failed responses/timeouts retry with the same ID and body but a fresh signature timestamp. No destination means events stay pending; registration can deliver an older backlog.

| Event type | CRM action |
| - | - |
| `subscription.ended` | Mark the allocation inactive; refresh current state before overwriting a newer subscription. |
| `account.deleted` | Mark the MAMX registration deleted, not the MT5 account. |
| `operation.done`, `operation.rejected`, `operation.review` | Fetch the operation by ID. REVIEW is not failure confirmation or permission to repeat a withdrawal. |
| `fee.debited`, `fee.paid` | Track investor debit separately from credit to the payment account. |
| `import.review`, `import.partial_review` | Escalate to the operator. Never create a compensating trade automatically. |

The envelope has `event_id`, `type`, UTC `occurred_at` and event-specific `data`. It contains no account passwords or bearer tokens.

## Event payload examples

Each example below is a complete JSON body POSTed to your receiver. IDs, logins, timestamps and amounts are illustrative. The examples are formatted for readability; verify the signature against the original bytes, not this formatting.

### Subscription ended

Mark this allocation inactive. The notification identifies both MT5 logins.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "event_id": "f04414108f834e21a23f192d6a74b300",
  "type": "subscription.ended",
  "occurred_at": "2026-11-03T14:00:00+00:00",
  "data": {
    "allocation_id": "b512abce4f964b61903062676bba213e",
    "leader_login": "230100",
    "follower_login": "230101",
    "status": "CANCELLED"
  }
}
```

### Account deleted

This closes the MAMX registration; it does not delete the MT5 user.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "event_id": "f04414108f834e21a23f192d6a74b301",
  "type": "account.deleted",
  "occurred_at": "2026-11-03T14:01:00+00:00",
  "data": {
    "account_login": "230101",
    "account_id": "1924c7c527a84d519f1ad961cc2eb1ce"
  }
}
```

### Operation completed: account creation

Fetch `GET /api/v1/mamx/operations/{operation_id}` for the result, including the new login. The webhook itself does not return credentials.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "event_id": "f04414108f834e21a23f192d6a74b302",
  "type": "operation.done",
  "occurred_at": "2026-11-03T14:02:00+00:00",
  "data": {
    "operation_id": "fe4124ede8944e7da482acbed14b7ed1",
    "kind": "ACCOUNT",
    "status": "DONE",
    "step": "DONE"
  }
}
```

### Operation completed: investor withdrawal

A withdrawal uses the same event type. Use `data.kind` to distinguish it from account creation.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "event_id": "f04414108f834e21a23f192d6a74b303",
  "type": "operation.done",
  "occurred_at": "2026-11-03T14:03:00+00:00",
  "data": {
    "operation_id": "fe4124ede8944e7da482acbed14b7ed2",
    "kind": "INVESTOR_WITHDRAW",
    "status": "DONE",
    "step": "DONE"
  }
}
```

### Operation rejected

Example: the existing MT5 account requested for registration was not found. Fetch the operation for the rejection reason.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "event_id": "f04414108f834e21a23f192d6a74b304",
  "type": "operation.rejected",
  "occurred_at": "2026-11-03T14:04:00+00:00",
  "data": {
    "operation_id": "fe4124ede8944e7da482acbed14b7ed3",
    "kind": "ACCOUNT_IMPORT",
    "status": "REJECTED",
    "step": "ACCOUNT_INTENT"
  }
}
```

### Operation needs review

The withdrawal outcome is uncertain. Do not issue another withdrawal.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "event_id": "f04414108f834e21a23f192d6a74b305",
  "type": "operation.review",
  "occurred_at": "2026-11-03T14:05:00+00:00",
  "data": {
    "operation_id": "fe4124ede8944e7da482acbed14b7ed4",
    "kind": "INVESTOR_WITHDRAW",
    "status": "REVIEW",
    "step": "WITHDRAW_INTENT"
  }
}
```

### Fee debited

The investor has been charged. This is not confirmation that the payment account has been credited.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "event_id": "f04414108f834e21a23f192d6a74b306",
  "type": "fee.debited",
  "occurred_at": "2026-11-03T14:06:00+00:00",
  "data": {
    "fee_id": "306ddbdb94e25864b41879a719ecceda",
    "allocation_id": "b512abce4f964b61903062676bba213e",
    "amount": "4.76"
  }
}
```

### Fee paid

The payment is confirmed. `credit_batch_id` is present for batch payments and omitted otherwise. This is the same fee as in the debit example, not a second charge.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "event_id": "f04414108f834e21a23f192d6a74b307",
  "type": "fee.paid",
  "occurred_at": "2026-11-03T14:07:00+00:00",
  "data": {
    "fee_id": "306ddbdb94e25864b41879a719ecceda",
    "allocation_id": "b512abce4f964b61903062676bba213e",
    "amount": "4.76",
    "credit_batch_id": "fe4124ede8944e7da482acbed14b7ed5"
  }
}
```

### Trade copy needs review

Identify the affected task and allocation when contacting the operator.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "event_id": "f04414108f834e21a23f192d6a74b308",
  "type": "import.review",
  "occurred_at": "2026-11-03T14:08:00+00:00",
  "data": {
    "task_id": "2a8fd24fd789429c84d9428f8c63bc86",
    "allocation_id": "b512abce4f964b61903062676bba213e",
    "step": "EXIT_INTENT"
  }
}
```

### Partial close needs review

`data.event_id` identifies the source close; it is **not** the webhook ID. Use the top-level `event_id` for delivery deduplication. `remaining_volume` can be null when the volume is unknown.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "event_id": "f04414108f834e21a23f192d6a74b309",
  "type": "import.partial_review",
  "occurred_at": "2026-11-03T14:09:00+00:00",
  "data": {
    "event_id": "8cd49824ad2dc061b04ab05655544b8d618d798258276ecb740b020771129ac5",
    "allocation_id": "b512abce4f964b61903062676bba213e",
    "root_task_id": "2a8fd24fd789429c84d9428f8c63bc86",
    "remaining_volume": "0.24"
  }
}
```


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