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

# API reference

> Endpoints, parameters, responses and error codes.

Both hosts use Entra ID bearer tokens. The tenant comes only from the token (`tid`); no route, query or body value can select a tenant in the customer-facing API.

## Valedo.API

| Method | Path | Role | Purpose |
| - | - | - | - |
| GET | `/healthz` | anonymous | Liveness probe |
| GET | `/api/v1/signature` | any authenticated user of an onboarded tenant | Resolve and render the signature for the signed-in user |
| GET | `/api/admin/tenant` | `Valedo.User` | Licensing status of the caller's tenant |
| GET | `/api/admin/templates` | `Valedo.User` | The caller's templates without their HTML |
| GET | `/api/admin/templates/{templateId}` | `Valedo.User` | One template with its sanitized HTML and `eTag` |
| POST | `/api/admin/templates` | `Valedo.User` | Create a draft template |
| PUT | `/api/admin/templates/{templateId}` | `Valedo.User` | Replace a template's content (`If-Match` required) |
| DELETE | `/api/admin/templates/{templateId}` | `Valedo.User` | Delete a template that no rule selects |
| GET | `/api/admin/rules` | `Valedo.User` | The caller's rules by ascending priority |
| GET | `/api/admin/rules/{ruleId}` | `Valedo.User` | One rule with its `eTag` |
| POST | `/api/admin/rules` | `Valedo.User` | Create a rule |
| PUT | `/api/admin/rules/{ruleId}` | `Valedo.User` | Replace a rule's content (`If-Match` required) |
| DELETE | `/api/admin/rules/{ruleId}` | `Valedo.User` | Delete a rule |

### GET /api/v1/signature

| Query | Type | Default | Notes |
| - | - | - | - |
| `context` | `new` \| `reply` \| `forward` | `new` | Compose context used by rules |
| `from` | string | none | Sending mailbox, used by sender-mailbox rules |

| Status | Meaning |
| - | - |
| 200 | Body is the rendered signature (`SignatureResponse`). Response carries an `ETag`. |
| 204 | Insert nothing. The add-in never shows an error in the event runtime. |
| 304 | The `If-None-Match` header matches the current `ETag`; no body. |
| 402 | No active Marketplace subscription (`license_inactive`). |
| 403 | Caller lacks the required role (`forbidden`). |

### GET /api/admin/tenant

| Status | Meaning |
| - | - |
| 200 | Tenant lifecycle status (`TenantStatusResponse`), including `transitionEndsAtUtc`, `canExtendTransition`, `retrievalEndsAtUtc` and `readOnly` after cancellation. Also answered during the read-only retrieval period. |
| 402 | `license_inactive` (pending or suspended). |
| 403 | `forbidden`. |

### Templates and rules

Templates and rules live in the Cosmos DB `config` container of the caller's tenant. Reads are allowed in every readable lifecycle state, including the retrieval period; changes are refused in the retrieval period (`tenant_read_only`). `Valedo.Admin` includes every `Valedo.User` permission.

**Concurrency.** Every template and rule response carries an `eTag`. `PUT` requires it in the `If-Match` header: a missing header returns 400 `validation_failed`, a stale one returns 409 `concurrency_conflict`. Read the document again and retry.

**Templates.** Request body of `POST` and `PUT` (`SaveTemplateRequest`):

| Field | Type | Rules |
| - | - | - |
| `name` | string | Required, at most 200 characters. |
| `description` | string | Optional, at most 1,000 characters. |
| `html` | string | Required, at most 65,536 characters. Sanitized on save against an email-safe allow-list (table layout, basic text formatting, links, images; inline CSS from a short list; `http`, `https`, `mailto`, `tel` and `cid` URLs). Scripts, forms, iframes, event handlers and `javascript:` or `data:` URLs are removed, so the stored `html` can differ from what was sent. |
| `plainText` | string | Optional, at most 65,536 characters. |

A new template is a `draft` with `currentVersion` 0. Publishing (immutable versions and the compiled snapshot) is not part of these endpoints yet, so `PUT` edits the working copy and leaves `status` and `currentVersion` unchanged. `DELETE` returns 409 `template_in_use` while any rule selects the template.

**Rules.** Request body of `POST` and `PUT` (`SaveRuleRequest`):

| Field | Type | Rules |
| - | - | - |
| `name` | string | Required, at most 200 characters. |
| `enabled` | boolean | Default `false`. |
| `priority` | integer | 0–10,000; lower wins. Default 0. When `enabled` is true, no other enabled rule of the tenant may use the same priority (400 `validation_failed`). |
| `templateId` | string | Required; must be a template of the caller's tenant, otherwise 400 `validation_failed`. |
| `appliesTo` | array of `new` \| `reply` \| `forward` | At least one; duplicates are dropped. |

Only these fields exist so far; sender, schedule and condition rules (README §5.2) are added with the rule engine.

| Status | Meaning |
| - | - |
| 200 / 201 / 204 | Read or update succeeded / created (with `Location`) / deleted. |
| 400 | `validation_failed` (field errors in `errors`), `request_body_required`, or a missing `If-Match`. |
| 402 | `license_inactive`. |
| 403 | `forbidden`: the role is missing. |
| 404 | `not_found`: no such template or rule in the caller's tenant. |
| 409 | `concurrency_conflict`, `template_in_use` (template delete) or `tenant_read_only`. |

### POST /api/marketplace/webhook

Called by Microsoft Marketplace, never by customers. Authorization is a separate bearer scheme: `aud` = the `Valedo Marketplace` app ID, `tid` = the publisher tenant, `appid` or `azp` = `20e940b3-4c77-4b0b-9a53-9e16a1b010a7` (Marketplace SaaS API). The body is read tolerantly (only `id`, `action`, `subscriptionId`) and stored as is; the Worker confirms it with Get Operation before acting (README §4.8). The body contains email addresses and is never logged.

| Status | Meaning |
| - | - |
| 200 | Stored, or already stored by an earlier retry. |
| 400 | `change_quantity_not_supported` (rejects the quantity change within Microsoft's 10 seconds), or `validation_failed` (body lacks `id`, `action` or `subscriptionId`). |
| 401 / 403 | Token missing, invalid, or not from the Marketplace SaaS API for our tenant. |

## Publisher API

| Method | Path | Role | Purpose |
| - | - | - | - |
| GET | `/healthz` | anonymous | Liveness probe |
| GET | `/api/publisher/dashboard` | `Valedo.Publisher.Staff` (any publisher role) | Tenant counts by lifecycle state |
| GET | `/api/publisher/tenants` | `Valedo.Publisher.Support` (Admin also) | One page of tenants, optionally filtered |
| GET | `/api/publisher/tenants/{tenantId}` | `Valedo.Publisher.Support` (Admin also) | One tenant from the registry |
| GET | `/api/publisher/marketplace-events` | `Valedo.Publisher.Support` (Admin also) | One page of stored Marketplace webhook events, newest first |
| GET | `/api/publisher/marketplace-events/{eventId}` | `Valedo.Publisher.Support` (Admin also) | One stored Marketplace webhook event |

The Publisher API is a separate host and token audience, restricted to users of the publisher's own Entra tenant with publisher roles (`Valedo.Publisher.Admin`, `Valedo.Publisher.Support`, `Valedo.Publisher.Finance`). Tokens are issued for `api://{Publisher Console app ID}/access_as_user`. Tenant IDs in routes are accepted on this host only, because it is the one place allowed to read across customers.

### GET /api/publisher/tenants

| Query | Type | Default | Notes |
| - | - | - | - |
| `pageSize` | integer 1–100 | 50 | Each partition returns at most this many, so a page can be shorter. |
| `continuationToken` | string | none | `continuationToken` from the previous page. |
| `status` | `pending` \| `active` \| `suspended` \| `transition` \| `retrieval` | none | Only tenants in that state (README §4.8). |
| `search` | string | none | Case-insensitive name fragment, or an exact tenant ID. |

| Status | Meaning |
| - | - |
| 200 | `TenantPageResponse`: `items` and `continuationToken` (null on the last page). |
| 400 | `validation_failed`, for example `pageSize` out of range. |
| 403 | `forbidden`. |

### GET /api/publisher/tenants/{tenantId}

| Status | Meaning |
| - | - |
| 200 | `TenantDetailResponse`: registry facts only. Template and rule content is never returned. |
| 403 | `forbidden`. |
| 404 | `not_found`: no tenant with this ID in the registry. |

### GET /api/publisher/marketplace-events

Reads the raw webhook events Valedo.API stored in the `publisher` container, so staff can find events that need a person.

| Query | Type | Default | Notes |
| - | - | - | - |
| `pageSize` | integer 1–100 | 50 | Single-partition query, so pages are full until the last one. |
| `continuationToken` | string | none | `continuationToken` from the previous page. |
| `status` | `received` \| `retrying` \| `processed` \| `ignored` \| `deadLettered` | none | Only events in that processing state. |

| Status | Meaning |
| - | - |
| 200 | `MarketplaceEventPageResponse`: `items` and `continuationToken` (null on the last page). |
| 400 | `validation_failed`, for example `pageSize` out of range. |
| 403 | `forbidden`. |

### GET /api/publisher/marketplace-events/{eventId}

`eventId` is the Marketplace operation ID.

| Status | Meaning |
| - | - |
| 200 | `MarketplaceEventResponse`: action, subscription ID, `status`, `attempts`, `outcome`, confirmed `tenantId` and timestamps. The raw body is never returned, because it contains purchaser and beneficiary email addresses. |
| 403 | `forbidden`. |
| 404 | `not_found`: no stored event with this ID. |

## Errors

Errors are RFC 7807 ProblemDetails with a stable snake\_case `errorCode` and a `traceId`. The constants live in `src/Valedo.Domain/Exceptions/ErrorCodes.cs`; the HTTP mapping is in `src/Valedo.Api/Middleware/GlobalExceptionHandler.cs`. New codes are added together with the feature that raises them, and documented here.

| `errorCode` | HTTP | Meaning |
| - | - | - |
| `tenant_not_onboarded` | 403 | The caller's tenant is not present or not active in the tenant registry. |
| `license_inactive` | 402 | The tenant has no active subscription; the add-in inserts nothing. |
| `forbidden` | 403 | Authenticated, but the required app role is missing. |
| `validation_failed` | 400 | The request body failed validation; field errors are in `errors`. |
| `request_body_required` | 400 | The endpoint expects a JSON body and none was sent. |
| `not_found` | 404 | The resource does not exist in the caller's tenant (Valedo.API) or in the registry or event store (Publisher API). |
| `concurrency_conflict` | 409 | The document changed since it was read (ETag mismatch). |
| `template_in_use` | 409 | A template cannot be deleted while a rule still selects it. |
| `tenant_read_only` | 409 | The tenant is in its retrieval period after cancellation; data can be read and exported but not changed. |
| `change_quantity_not_supported` | 400 | Marketplace webhook only: a `ChangeQuantity` event, which this offer rejects (no seats). |
| `lifecycle_conflict` | 409 | The lifecycle change isn't allowed in the tenant's current state, for example a second transition extension. |
| `marketplace_api_failed` | 500 | Worker only: the Marketplace Fulfillment API answered with an unexpected status. Stored on the webhook event as its `outcome`; never returned by an endpoint. |
| `internal_error` | 500 | Unexpected failure; details are hidden outside Development. |

## OpenAPI

The Valedo.API emits its OpenAPI document to `src/Valedo.Api/openapi/Valedo.Api.json` on build. Only Valedo.API paths appear there; the `/healthz` probes are documented on this page.


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