# Save a Tenant Webhook

Make or change the webhook of one environment. The secret is shown when the webhook is made, and never again.

## Overview

- **`201` with `webhook` and `secret`** when the environment had no webhook. Store the secret now: it signs every delivery and is never shown again (see [Replace the Secret](https://docs.aureahub.com/docs/tenant-webhook-rotate.md)).
- **`200` with `webhook`** when it changes an existing one. The secret stays, and so does the status when the body doesn't name one.
- `url`: `https://` on the default port with a full public DNS name — the rules are in [Events to Your Server](https://docs.aureahub.com/docs/guide-events.md). `eventTypes`: the types it receives, at most five; `[]` receives all of them. A type named twice counts once, and the answer lists the types in a fixed order. `status`: `active` (the default for a new webhook) or `disabled`.
- The body is checked as sent: a field that doesn't belong, or a string where a list goes, is refused rather than ignored or converted.
- For an Aurea administrator, or the tenant's own administrator. Every save is written to the tenant's activity log with what it replaced, never with the secret.

## Endpoint

### `PUT /v1/admin/tenants/{id}/webhooks/{environment}`

Authentication: bearer token required.

Makes the environment's webhook (201, with the secret) or changes it (200).

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Tenant UUID |
| `environment` | string | yes | `production` or `sandbox` |

**Request body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string | yes | Your server's address, up to 2048 characters |
| `eventTypes` | string[] | yes | Any of `ramp.kyc.updated`, `ramp.deposit.updated`, `ramp.payout.updated`, `ramp.transaction.updated`, `ramp.card_session.updated`; empty for all |
| `status` | string | no | `active` or `disabled`. Omitted: `active` for a new webhook, unchanged for an existing one |

**Responses**

`201` Made

```json
{
  "webhook": {
    "id": "5c03a06e-55f0-425a-8562-bd7bae35825d",
    "environment": "production",
    "url": "https://hooks.example.com/aurea/events",
    "eventTypes": ["ramp.deposit.updated", "ramp.payout.updated"],
    "status": "active",
    "secretHint": "q0Rk",
    "secretRotatedAt": "2026-09-17T09:02:11.030Z",
    "createdAt": "2026-09-17T09:02:11.030Z",
    "updatedAt": "2026-09-17T09:02:11.030Z"
  },
  "secret": "whsec_… (44 characters of base64, ending q0Rk=)"
}
```

`200` Changed

```json
{
  "webhook": {
    "id": "5c03a06e-55f0-425a-8562-bd7bae35825d",
    "environment": "production",
    "url": "https://hooks.example.com/aurea/events",
    "eventTypes": [],
    "status": "active",
    "secretHint": "q0Rk",
    "secretRotatedAt": "2026-09-17T09:02:11.030Z",
    "createdAt": "2026-09-17T09:02:11.030Z",
    "updatedAt": "2026-09-17T09:05:40.072Z"
  }
}
```

`400` Refused

```json
{
  "statusCode": 400,
  "error": "BadRequestError",
  "message": "The webhook URL must use https",
  "details": { "code": "NOAH_WEBHOOK_INVALID", "field": "url", "reason": "not_https" }
}
```

The `secret` is `whsec_` followed by the base64 of 32 random bytes; the example above shortens it.

## Refusals

Nothing is saved, and the answer is `400` with `details.code` `NOAH_WEBHOOK_INVALID` and the field at fault in `details.field`:

- `url` an address Aurea won't call, with `details.reason`: `not_https`, `port`, `ip_address`, `local_host`, `host_name` (not a full DNS name), `credentials`, `fragment`, `whitespace_or_control`, `backslash`, `dot_segment` or `not_a_url`. An empty address, one longer than 2048 characters or one that isn't a string has `details.issues` instead of a reason.
- `eventTypes`: an unknown type, more than four entries, or not a list.
- `status`: neither `active` nor `disabled`.
- `body`: a field that doesn't belong, for example `secret` — the API always makes the secret.
- `environment`: neither `production` nor `sandbox`. On the other webhook endpoints such an environment answers `400` `Request validation failed`.

With a field that doesn't match its type, `details.issues` lists each problem with its path. `details.code` `NOAH_WEBHOOK_CONFLICT` (`400`) means another request made the same environment's webhook at the same moment: send the change again.

---

Web version: https://docs.aureahub.com/#tenant-webhook-save
