# Business Management

> ⚠️ **Preview — this API is not live yet.** Planned release: **October 2026**. The endpoint surface is not deployed; paths and schemas may change before the contract freeze, and try-it and production calls are unavailable.

> **Coming soon — October 2026.** This is a contract prototype, not a working
> API. The [V3 Prototype Guide](quickstart.md) is for implementation planning;
> its requests cannot be executed today.

# Overview

The Business Management API V3 is Altegio's next-generation public REST API.
This document is a **preview of the V3 contract**: it describes the target API surface so
partners and tooling can review it before the endpoints go live.

**Preview status — please read first:**

- This is a **contract preview, not a live production API**. The endpoint surface is not
  deployed yet; try-it consoles and production calls are unavailable.
- Paths, schemas, and field names **may change before the contract freeze** without
  backward-compatibility guarantees.
- Every operation carries an `x-altegio-status`. Three values are defined:
  `planned` — designed, not scheduled for the first production release;
  `preview` — contract published for review, not deployed;
  `available` — live in production and covered by the October 2026 release promise.
  **Today every operation is `preview`** — none of them is deployed yet. The October
  release covers only operations marked `available`, so publishing the full target
  surface here is not a promise that all of it ships at once.
  `available` is enforced by CI in `biz.erp`: an `available` operation without a registered
  route fails the OpenAPI conformance gate.
- Where a design decision is still pending, the affected element also carries
  `x-altegio-assumption` describing the open question.

**Base URL (resource API):** `https://api.alteg.io/api/v3`

All resources are under `/api/v3` and scoped to a Location:
`/api/v3/locations/{location_id}/...`. The `location_id` in the path is verified against
the credential used for the call — a Location the credential cannot access responds with
`404`. OAuth and discovery endpoints live on the authorization host root
`https://api.alteg.io` (no `/api/v3` prefix) — see the Authorization tag.

**Canonical terms.** V3 uses one public name per entity:

| Term | Meaning |
|---|---|
| `location` | A business location (branch). |
| `team_member` | A person on the Location's team who provides services. |
| `service` | A bookable service offered at a Location. |
| `client` | A person served by a Location — the person appointments are scheduled for. |
| `appointment` | A scheduled visit entry for one Team Member. |
| `visit` | The billing unit that groups appointments and carries items and payments. |

**Response format.** V3 is flat resource REST. Single resources are plain
JSON objects with integer `id` and a string `object` type marker. Lists use one predictable
envelope. V3 does **not** use JSON:API (`data.attributes`) or wrapper envelopes such as `{success,data,meta}`
envelopes.

**Object types.** Every resource declares its kind in the `object` field — the full
dictionary of this preview:

| `object` | What it is |
|---|---|
| `location` | A business location and its settings. |
| `service` / `service_category` | Catalog: a bookable service (simple or package) and its category. |
| `team_member` / `position` | A person on the team and the position dictionary. |
| `client` | A person served by a Location. |
| `availability_slot` | A bookable time slot (computed, read-only). |
| `resource` / `resource_instance` | Bookable resources (rooms, equipment) and their instances. |
| `appointment` | A scheduled entry for one Team Member. |
| `visit` / `visit_item` | The billing unit grouping appointments; its service/product lines. |
| `payment` / `refund` | A payment applied to a visit and its local reversal. |
| `payment_method` / `account` | Payment configuration dictionaries of the Location. |
| `product` | A sellable product from the Location catalog (minimal P0 read). |
| `accessible_resource` | A Location or chain grant visible on the current token. |
| `team_member_access` | System-access status and role for a Team Member. |
| `appointment_create_result` | Package booking result with multiple appointments. |
| `list` | The list envelope wrapping any collection response. |

# Authorization

V3 accepts exactly one credential shape on every call:

```
Authorization: Bearer <token>
```

Tokens are **opaque** strings — never parse or decode them. There is **no public password
grant**: applications never collect Altegio passwords. Three kinds of credential exist. The
first two ship in the first iteration; the third is planned for the second delivery.

## API key (your own scripts and agents)

For a person automating their own work — a script, a private MCP server, an agent acting
for them:

1. Any Business User creates an **API key** in the Developer Portal: they pick the Locations
   and chains they have access to, the scopes, a name and an expiry (required, a date at most
   one year out, read as the end of that day in UTC). The secret `ag_key_…` is shown exactly
   once.
2. The key itself is the Bearer credential — there is **no token exchange**:
   `Authorization: Bearer ag_key_…`.
3. The key **acts on behalf of its author**. Every call checks the key's grants on the
   target Location or chain and the author's *current* business permissions there. When the
   author loses a permission or a Location, the key loses it too; when the author's account
   is removed, the key stops working everywhere. Nothing is copied into the key.
4. The author sees and revokes only their own keys; revocation takes effect on the next
   request. To change scopes, issue a new key and revoke the old one — there is no
   regeneration.

API keys work only on `/api/v3` routes. Scopes that hand out access to other people
(`team_members:manage_access`, `chain_access:manage`) and other dangerous scopes are never
available to an API key — they are simply absent from the portal.

## Authorization Code + PKCE (user-delegated: agents, MCP hosts, applications)

For applications and agent hosts acting on behalf of a specific Business User:

1. Redirect the user's browser to `GET /oauth/authorize` with `client_id`, `redirect_uri`,
   `scope`, `state`, and a PKCE `code_challenge` (S256).
2. The user logs in with their Altegio account.
3. A **consent screen** shows who is asking, lists the requested scopes as a whole, and lets
   the user choose the Locations and chains the client may act in; the user grants or declines.
4. The browser is redirected back to your `redirect_uri` with a short-lived, one-time
   authorization `code` (and your `state`).
5. Exchange the code at `POST /oauth/token` with `grant_type=authorization_code` and the
   PKCE `code_verifier` — the response contains an access token (valid for one hour) and a
   **rotating refresh token** (30 days per token, 90 days per family).
6. Refresh with `grant_type=refresh_token`. Every use rotates the refresh token; replaying
   an already-used refresh token revokes the whole token family.

`GET /oauth/authorize` is a **browser web flow** (HTML login and consent pages), not a JSON
operation — it is documented here in prose only. The delegated token acts with the current
business permissions of the user who granted it, exactly like an API key acts with its
author's. `GET /oauth/accessible-resources` returns the Locations, chains and scopes of the
current token; it answers to API keys as well.

OAuth client registration: official and known integrations are pre-registered as OAuth
clients owned by a developer account. MCP hosts and other OAuth clients self-onboard through
**either** mechanism, and both are supported. A `client_id` given as an HTTPS URL pointing to
a client metadata document (CIMD) is the main road; Dynamic Client Registration
(`POST /oauth/register`) stands beside it for hosts that have no public domain to serve a
metadata document from, and for hosts that have not moved to CIMD yet. Which one a host takes
is the host's choice, not ours: a client only attempts the URL form when
`client_id_metadata_document_supported` is advertised in discovery, and some hosts go straight
to registration by default. Registration creates an **OAuth client identity only** — it does not
create a marketplace application, an installation, a business grant, or any access to data;
those come from user consent. Self-registered clients receive base scopes only.

**`client_id` means two different things depending on the surface** — the word `client` is
overloaded, so this contract keeps them apart:

| Surface | Entity | Identifier |
|---|---|---|
| Resource API (`/clients`) | the person the business serves | `client_id` of a `/clients` resource |
| OAuth (`/oauth/*`, `/.well-known/*`) | OAuth client registration | OAuth `client_id`, e.g. `ag_client_9c41` or a CIMD URL |
| Marketplace / Developer Portal | the developer's application | application id — never sent on the wire in V3 |

OAuth `client_id` is the standard field name from RFC 6749 and is not renamed; prose in the
authorization sections always says **OAuth client** when it means the protocol entity.

## Client Credentials + `location_id` (marketplace applications) — planned

For marketplace applications acting autonomously in the Locations that installed them. This
flow is **planned for the second delivery** and is not part of the first iteration; the
contract is published so application developers can prepare:

1. A business installs your application in a Location and approves the scopes your
   application requests.
2. Your backend calls `POST /oauth/token` with `grant_type=client_credentials`, HTTP Basic
   client authentication (`client_id` / `client_secret`), and the explicit `location_id` you
   want to act in. One token is bound to **one** Location — an application installed in
   twenty Locations requests twenty tokens, and there is no account-wide token.
3. The token **acts on behalf of the installation**, within the scopes the business
   approved at install time. Access tokens last one hour;
   this flow has **no refresh token** — request a new access token when the current one
   expires.

**Not every scope exists in every credential class.** The `scopes` list published under each
OAuth flow is the set that flow can actually be granted — requesting anything outside it
fails with `invalid_scope`. API keys use the same public catalog with the exclusions
called out here:

| Scope | Availability |
|---|---|
| The 12 first-iteration scopes | Every credential kind. |
| Scopes marked `Planned:` in the flow lists | Published in discovery as planned; a token may already be issued with them and starts working when the operation ships. |
| `locations:create` | Authorization Code only, planned. Never available to API keys: creating a Location is account-level. |
| `team_members:manage_access`, `chain_access:manage` | Handing out access to other people stays behind the human's own login. Documented here, but granted to nobody in this iteration and left out of `scopes_supported` until moderation for verified partners exists. |

Scopes are added to a flow or credential class, never removed: adding one later is
backward-compatible, so the published lists start conservative.

# Access model

Every request is resolved through the same runtime formula:

```
effective access = token scopes
                 ∩ grants of the credential on the target Location or chain
                 ∩ what the business allows behind that credential
                   (an API key: its author's rights; a delegated token: the rights
                    of the person who consented; an installation token: what the
                    business approved for the application)
                 ∩ tenant (Location)
```

- A **scope opens an endpoint**; the rights behind the credential then decide which rows
  and fields are visible or writable. Having a scope does not bypass the permission system
  of the business, and those rights can change without you: when a business admin narrows
  them, the credential narrows with them on the next request.
- **Location binding:** resources live under `/api/v3/locations/{location_id}/...`; the
  `location_id` in the route is checked against the token's grants. A foreign Location returns
  `404` — existence is not leaked.
- **Scopes are credential-class bounded.** A credential can use only scopes allowed for its
  kind. An API key and a user-delegated token act with the *live* permissions of the
  author or the consenting user, read on every request with a cache of at most one minute;
  an installation token acts with the permissions granted to the installation. Revocation
  is never cached: a revoked token fails on the very next request.
- **No step-up in this preview.** Dangerous operations in this preview are protected by
  explicit scopes, the subject's business permissions, audit, and rate limits.
  A future human-confirmation flow will be added only with a complete challenge contract.

# Conventions

- **Resource shape:** a flat JSON object with integer `id`, a string `object` type marker,
  and domain fields. No envelopes, no prefixes on ids.
- **List shape:** `{"object": "list", "data": [...], "has_more": false, "next_cursor": null}`.
  Pagination is cursor-based: pass `limit` and the `next_cursor` value from the previous
  page as `cursor`. Cursors are opaque, expire, and only work for the credential and filter
  set that produced them. Page/offset pagination does not exist in V3.
- **Field naming:** all JSON fields, query parameters, and path segments are `snake_case`;
  `operationId` values are `lowerCamelCase`.
- **Dates and times:** RFC 3339 everywhere.
  - Audit instants (`created_at`, `updated_at`, `cancelled_at`) are UTC: `2026-07-14T09:20:31Z`.
  - Appointment times (`starts_at`, `ends_at`) carry the Location's local UTC offset:
    `2026-07-09T11:30:00+01:00`.
  - Date-only values are `YYYY-MM-DD`; wall-clock times (working hours, schedule slots)
    are `HH:MM` in the Location's timezone.
  - Naive datetimes and Unix timestamps never appear.
- **Money:** integer **minor units** of the Location currency (`1250` = one thousand two hundred fifty minor units). The
  `currency` (ISO 4217) is server-controlled, read-only, and repeated in money-carrying
  resources. Monetary field names use the `_minor` suffix. Floating-point amounts are rejected.
- **Partial updates:** `PATCH` bodies are `application/merge-patch+json`. A missing field
  means "leave unchanged"; `null` clears a nullable field; arrays are cleared with an
  explicit `[]`, never `null`. Clearing a non-nullable field fails with `422
  field_not_nullable`; writing a read-only field fails with `422 field_immutable`. Unknown
  request fields are rejected; unknown response fields must be ignored by clients.
- **`expand[]`:** related objects are returned as ids by default. Documented relations can
  be expanded one level deep with `expand[]=<relation>` (allowlist per endpoint, depth 1).
  An unknown expand value fails with `422`. Expansion never bypasses field-level
  permissions — see PII below.

# Errors

Every error from a resource operation is `application/problem+json` (RFC 9457 hybrid):

| Field | Meaning |
|---|---|
| `type` | Stable URL identifying the problem type (links to documentation). |
| `title` | Human-readable summary of the problem type. |
| `status` | HTTP status code. |
| `detail` | Human-readable explanation of this occurrence. |
| `code` | **Stable machine-readable code** — branch on this, never on text. |
| `errors[]` | Field-level issues: `code`, `source`, `pointer`, `message`. `pointer` is a JSON Pointer for body errors and a parameter name for query/path/header errors. |
| `instance` | Request identifier for support and log correlation. Same value as the `X-Request-Id` header of that response, so quoting either one is enough. |

The OAuth protocol and discovery endpoints are the one exception: `POST /oauth/token`,
`POST /oauth/revoke`, `POST /oauth/register` and `/.well-known/*` return the standard OAuth 2.0
error object (`{"error": "…", "error_description": "…"}`) defined by RFC 6749, 7009 and 7591 —
not `application/problem+json`. The `GET /oauth/authorize` browser flow reports errors on its
own HTML page or as OAuth error parameters on the redirect, never as JSON. `GET
/oauth/accessible-resources` is the opposite case: despite the path it is an ordinary
Bearer-authed read and uses `application/problem+json` like the rest of the API.

Everything else in this section (stable codes, field-level `errors[]`, request correlation)
describes resource operations. Rate limiting is the one rule both surfaces share: every `429`
carries `Retry-After`.

Two conventions keep this table stable:

- **Promotion.** Only public machine-readable codes that clients are expected to branch on
  enter this table. Adding a code means editing this table and registering it in the biz.erp
  `V3ProblemCatalog` in the same cycle — the CI conformance gate keeps both sides in sync.
  Internal exceptions are never promoted implicitly: an unregistered code degrades to the
  generic status default and nothing internal leaks.
- **Naming.** `invalid_*` means the supplied value does not exist or fails validation
  (`invalid_token`, `invalid_city`, `invalid_business_type`); `unsupported_*` means the value
  is valid but not supported by the product or policy (`unsupported_country`,
  `unsupported_media_type`, `unsupported_token_subject`).

Stable codes used by this preview (branch on these; domain codes below are equally stable):

| Code | HTTP | When |
|---|---:|---|
| `bad_request` | 400 | Generic client reject without a more specific registered code (status default for bare 400 and unknown 4xx). Does **not** mean broken JSON. |
| `malformed_request` | 400 | Protocol-only: broken JSON / unparseable body / wire-format syntax. Thrown explicitly — never the catch-all for every 400. |
| `invalid_token` | 401 | Missing, expired, or revoked token. |
| `missing_scope` | 403 | The token does not carry the required scope. |
| `permission_denied` | 403 | Scope present, but the subject lacks the business permission. |
| `user_not_verified` | 403 | Location create blocked: the user account is not verified. |
| `location_creation_forbidden` | 403 | Location create is not allowed for this subject. |
| `unsupported_token_subject` | 403 | This credential class cannot perform the operation (e.g. installation token on `locations:create`). |
| `resource_not_found` | 404 | Generic 404 fallback: unknown `/api/v3/*` path, gate/FF hide, or tenant policy without a safe resource-specific code. |
| `{resource}_not_found` | 404 | Resource absent or hidden by tenant policy (e.g. `appointment_not_found`, `location_not_found`). |
| `method_not_allowed` | 405 | HTTP method is not supported on an existing v3 route. Emitted by routing before any operation is selected, so it is deliberately not declared per operation; the body is the same `Problem` shape as every other error. The response carries `Allow` with the methods the route does accept, e.g. `Allow: GET`. |
| `not_acceptable` | 406 | `Accept` cannot be satisfied. |
| `conflict` | 409 | Generic 409 fallback when the domain exception has no more specific registered code. |
| `slot_taken` | 409 | Appointment create lost the race for the requested time slot. |
| `idempotency_key_reused` | 409 | Same `Idempotency-Key` with a different request body. |
| `idempotency_request_in_progress` | 409 | Same key while the first request is still running (`Retry-After` included). |
| `duplicate_location` | 409 | Location create conflicts with an existing Location. |
| `last_location_owner` | 409 | Cannot revoke the last owner of the Location. |
| `invitation_already_pending` | 409 | A distinct pending invitation already exists. |
| `access_already_active` | 409 | Team Member already has active system access. |
| `visit_already_settled` | 409 | Visit is fully paid; adding a line would leave an unpaid remainder. |
| `stale_version` | 412 | `If-Match` did not match — the resource was changed by another writer. |
| `unsupported_media_type` | 415 | Request `Content-Type` is not supported. |
| `validation_failed` | 422 | Semantic field validation failed (details in `errors[]`). |
| `phone_taken` | 422 | Client phone already belongs to another client of the Location (under `clients:write`; audited and rate-limited). |
| `exceeds_unpaid` | 422 | Payment amount exceeds the visit's unpaid remainder. |
| `immutable_after_finance` | 422 | Country/currency cannot change after financial data exists. |
| `field_not_nullable` | 422 | Merge-patch tried to `null` a non-nullable field. |
| `field_immutable` | 422 | Merge-patch tried to change a read-only field. |
| `role_not_assignable` | 422 | Requested access role is not assignable in this Location. |
| `invitation_contact_required` | 422 | Invitation delivery needs a contact and none is available. |
| `invalid_city` | 422 | `city_id` is invalid for location create. |
| `unsupported_country` | 422 | Country derived from city is not supported. |
| `invalid_business_type` | 422 | `business_type_id` is invalid. |
| `location_quota_exceeded` | 422 | Account location quota exceeded. |
| `instrument_not_applicable` | 422 | Gift card / membership / loyalty instrument is not usable for this visit (unified; does not distinguish unknown vs wrong tenant). |
| `precondition_required` | 428 | Required `If-Match` header is missing. |
| `rate_limit_exceeded` | 429 | Rate limit hit (`Retry-After` included). |
| `location_creation_rate_limited` | 429 | Location create rate limit hit. |
| `internal_error` | 500 | Unexpected server error. |
| `dependency_unavailable` | 503 | A required dependency is down; the write was not performed (fail closed). |

Field-level codes inside `errors[]` (not top-level `code`) include `expired_cursor`,
`invalid_cursor`, `foreign_cursor`, and validation codes such as `invalid_datetime`.

Example — validation failure (`422`):

```json
{
  "type": "https://developer.alteg.io/problems/validation-error",
  "title": "Request validation failed",
  "status": 422,
  "detail": "One or more fields are invalid.",
  "instance": "urn:altegio:request:req_abc123",
  "code": "validation_failed",
  "errors": [
    {
      "code": "invalid_datetime",
      "source": "body",
      "pointer": "/starts_at",
      "message": "starts_at must be a valid RFC 3339 datetime."
    }
  ]
}
```

Example — stale optimistic-concurrency version (`412`):

```json
{
  "type": "https://developer.alteg.io/problems/stale-version",
  "title": "Resource version is stale",
  "status": 412,
  "detail": "The resource was modified after the version supplied in If-Match. Re-read it and retry.",
  "instance": "urn:altegio:request:req_def456",
  "code": "stale_version"
}
```

Retry guidance depends on the code, not on a generic flag: fix the request on `422`;
re-read the resource on `412`; retry the same `Idempotency-Key` after `Retry-After` on
`409 idempotency_request_in_progress`; back off on `429`/`503`; after a network timeout on
a create or command, retry **only with the same `Idempotency-Key`**.

# Idempotency & concurrency

Both mechanisms below belong to the contract but are **not part of the first iteration**: no
first-iteration operation requires `Idempotency-Key` or `If-Match`, and reads do not return
`ETag` yet. They arrive together with the operations that need them (appointment creation,
payments, concurrent appointment edits) and before contract freeze; operations that already
carry the headers in this document are marked `planned`.

**`Idempotency-Key`** will be required on every `POST` create and command where a retry could
duplicate a side effect (creating appointments, clients, payments; cancel/status commands).

- Generate a unique key per logical request (UUID recommended).
- A retry with the same key and the same body returns the **original result** — including
  the case where the first attempt committed but its response was lost.
- The same key with a different body fails with `409 idempotency_key_reused`.
- A concurrent duplicate fails with `409 idempotency_request_in_progress` and `Retry-After`.
- Keys are retained for **at least 24 hours**, scoped to the credential, Location (when
  present), and operation. After expiry the same key value may start a **new** logical
  request (it is not `idempotency_key_reused`).
- If the idempotency store is unavailable, the write is not performed: `503
  dependency_unavailable`.

**ETag / `If-Match`** will protect mutable resources from two writers silently overwriting
each other (for example, a human and an agent editing the same appointment):

1. `GET` a mutable resource — the response carries an `ETag` header (creates and updates
   return the fresh `ETag` too).
2. Send `PATCH` or `DELETE` with `If-Match: <etag>`.
3. On success the response carries the new `ETag`. If the resource changed in between, the
   call fails with `412 stale_version` — re-read, reapply your change, retry. A missing
   `If-Match` where it is required fails with `428 precondition_required`.

# PII & redaction

Client contact fields — `phone`, `email`, `additional_phone` — are sensitive:

- Reading them requires the `clients:read_contact` scope **and** the corresponding business
  permission of the token subject.
- When access is refused, a contact field is returned as `null` and its name is listed in `redacted_fields` of the same object. It is never replaced with a mask, a placeholder or an empty string, so an integration cannot write a redacted value back.
- `expand[]=client` follows the same rule: expanding a client on an appointment or visit
  without contact access returns the client with redacted contact fields. Expansion never
  widens access.
- **`422 phone_taken` on create/update** is part of `clients:write` (no extra
  `clients:read_contact`). It only signals that the phone is already owned by another
  client of this Location — it does not return that client's identity or contact payload.
  Phone-bearing writes are audited and share an anti-enumeration rate limit with exact
  contact search so the existence signal cannot be bulk-probed.

# HTTP negotiation and caching

Clients should send `Accept: application/json` for successful resource responses and `application/problem+json` for errors; unsupported response media types fail with `406 not_acceptable`. JSON is UTF-8; `Content-Type` may include `charset=utf-8`. Read operations may include `Cache-Control` appropriate to the resource (`no-store` for PII-bearing reads, short `private` or `public` max-age for dictionaries and metadata). `ETag` and `If-Match` are planned (see Idempotency & concurrency); when they ship, mutable resource reads return strong ETags and `If-Match: *` is not accepted on PATCH/DELETE — clients re-read and send the concrete strong ETag.


Version: 3.0.0-preview
License: Altegio API Agreement

## Servers

Preview resource API contract - endpoint surface is not live yet
```
https://api.alteg.io/api/v3
```

## Security

### OAuth2

[object Object],[object Object]

Type: oauth2
Token URL: https://api.alteg.io/oauth/token
Scopes:
- `locations:read`: Planned: view location info and resources
- `locations:write`: Planned: manage location settings
- `services:read`: Planned: view services and service categories
- `services:write`: Planned: manage services and service categories
- `team_members:read`: Planned: view team members, their schedules, service links and positions
- `team_members:write`: Planned: add, update and dismiss team members; manage service links and positions
- `team_members:schedule`: Planned: manage the working schedules of team members
- `clients:read`: Planned: view clients without contact details
- `clients:read_contact`: Planned: view client phone numbers and email
- `clients:write`: Planned: manage clients
- `availability:read`: Planned: view bookable time slots
- `appointments:read`: Planned: view appointments
- `appointments:create`: Planned: create appointments
- `appointments:write`: Planned: edit, cancel and change the status of appointments
- `checkout:read`: Planned: view payment methods and the billing state of a visit
- `checkout:capture`: Planned: record payments
- `checkout:cancel`: Planned: cancel recorded payments locally
- `inventory:sell`: Planned: add products to a visit
- `products:read`: Planned: view the product catalog
- `finance:read`: Planned: view cash registers and financial accounts
- `loyalty:read`: Planned: view loyalty instruments

### ApiKey

[object Object]

Type: http
Scheme: bearer

## Download OpenAPI description

 - [Business Management](https://developer.alteg.io/_bundle/en/b2b-v3/openapi.yaml)

## Authorization

 - [GET /.well-known/oauth-authorization-server](https://developer.alteg.io/en/b2b-v3/openapi/authorization/getoauthauthorizationservermetadata.md): Machine-readable OAuth 2.0 authorization server metadata (RFC 8414): endpoint URLs, supported grant types, PKCE methods, and scopes. Agents and MCP hosts use this document to discover the authorizatio
 - [POST /oauth/token](https://developer.alteg.io/en/b2b-v3/openapi/authorization/createoauthtoken.md): OAuth 2.0 token endpoint (RFC 6749). Supports three grants: - `authorization_code` — exchange a code obtained from the browser `GET /oauth/authorize` flow (PKCE `code_verifier` required). Returns an a
 - [POST /oauth/revoke](https://developer.alteg.io/en/b2b-v3/openapi/authorization/revokeoauthtoken.md): OAuth 2.0 token revocation (RFC 7009). Revokes an access token or a refresh token; revoking a refresh token revokes its whole token family. The endpoint returns `200` even when the token is already in
 - [POST /oauth/register](https://developer.alteg.io/en/b2b-v3/openapi/authorization/registeroauthclient.md): Minimal Dynamic Client Registration (RFC 7591) for MCP hosts and other clients that are not pre-registered. Registration only issues a `client_id` — access to any data still requires the user login/co
 - [GET /oauth/accessible-resources](https://developer.alteg.io/en/b2b-v3/openapi/authorization/listaccessibleresources.md): Returns the resources **this token** is actually granted — not everything the user or application could reach. Each entry carries the scopes granted on that resource, so a client knows which `location
## Locations

 - [GET /reference/cities](https://developer.alteg.io/en/b2b-v3/openapi/locations/listcities.md): Cursor-paginated list. Default ordering: country_id, name, id tie-breaker. Cursors use the `cursor` query parameter, expire after one hour, and are bound to the credential, tenant, and filters. Expire
 - [GET /reference/business_types](https://developer.alteg.io/en/b2b-v3/openapi/locations/listbusinesstypes.md): Cursor-paginated list. Default ordering: title, id tie-breaker. Cursors use the `cursor` query parameter, expire after one hour, and are bound to the credential, tenant, and filters. Expired, malforme
 - [POST /locations](https://developer.alteg.io/en/b2b-v3/openapi/locations/createlocation.md): Creates a standalone Location for the current authenticated user. Authorization is user-delegated only (Authorization Code + PKCE); installation tokens and API keys are rejected. `country_code`, `time
 - [GET /locations/{location_id}](https://developer.alteg.io/en/b2b-v3/openapi/locations/getlocation.md): Returns the Location resource: name, address, contacts, timezone, and currency.
 - [PATCH /locations/{location_id}](https://developer.alteg.io/en/b2b-v3/openapi/locations/updatelocation.md): Partially updates partner-relevant Location settings with a merge-patch body. `timezone`, `country`, and `currency` are read-only and intentionally absent from the update schema — they follow `city_id
## Services

 - [GET /locations/{location_id}/services](https://developer.alteg.io/en/b2b-v3/openapi/services/listservices.md): Returns the services of the Location as a cursor-paginated list. Prices are integer minor units; `duration_seconds` is the default duration, and `team_members[]` carries per-member price/duration over
 - [POST /locations/{location_id}/services](https://developer.alteg.io/en/b2b-v3/openapi/services/createservice.md): Creates a service in the Location catalog. Link Team Members inline through `team_members[]` — a service without at least one bookable Team Member link does not appear in availability search. Service
 - [GET /locations/{location_id}/services/{service_id}](https://developer.alteg.io/en/b2b-v3/openapi/services/getservice.md): Returns a single service with its category, pricing, duration, Team Member links, and package components when `type` is `package`.
 - [PATCH /locations/{location_id}/services/{service_id}](https://developer.alteg.io/en/b2b-v3/openapi/services/updateservice.md): Partially updates a service with a merge-patch body. `type` is immutable; for package services, `package.components` replaces the full component list when present. Team Member links are written throug
 - [DELETE /locations/{location_id}/services/{service_id}](https://developer.alteg.io/en/b2b-v3/openapi/services/deleteservice.md): Deletes a service from the catalog.
 - [GET /locations/{location_id}/service_categories](https://developer.alteg.io/en/b2b-v3/openapi/services/listservicecategories.md): Returns the service categories of the Location. Use a category `id` as `category_id` when creating services. Cursor-paginated list. Default ordering: weight desc, name, id tie-breaker. Cursors use the
 - [POST /locations/{location_id}/service_categories](https://developer.alteg.io/en/b2b-v3/openapi/services/createservicecategory.md): Creates a service category.
 - [PATCH /locations/{location_id}/service_categories/{service_category_id}](https://developer.alteg.io/en/b2b-v3/openapi/services/updateservicecategory.md): Partially updates a service category with a merge-patch body.
 - [DELETE /locations/{location_id}/service_categories/{service_category_id}](https://developer.alteg.io/en/b2b-v3/openapi/services/deleteservicecategory.md): Deletes a service category.
## Products

 - [GET /locations/{location_id}/products](https://developer.alteg.io/en/b2b-v3/openapi/products/listproducts.md): Returns the minimal product catalog fields available in P0: `id`, `title`, `price_minor`, and `unit`. Inventory levels, balances, cost, and standalone product sales are not part of the preview scope.
## Team Members

 - [GET /locations/{location_id}/team_members](https://developer.alteg.io/en/b2b-v3/openapi/team-members/listteammembers.md): Returns the Team Members of the Location as a cursor-paginated list. The base representation contains name, position, and bookable services; Team Member personal contact data is not exposed in the pre
 - [POST /locations/{location_id}/team_members](https://developer.alteg.io/en/b2b-v3/openapi/team-members/createteammember.md): Creates a Team Member in the Location. After creating, set the working schedule (`PATCH .../schedule`) and link services (`PATCH .../services`) — otherwise the member does not appear in availability s
 - [GET /locations/{location_id}/team_members/{team_member_id}](https://developer.alteg.io/en/b2b-v3/openapi/team-members/getteammember.md): Returns a single Team Member.
 - [PATCH /locations/{location_id}/team_members/{team_member_id}](https://developer.alteg.io/en/b2b-v3/openapi/team-members/updateteammember.md): Partially updates the Team Member card with a merge-patch body: name, specialization, position and the paid-seat flag. Service links are written through `updateTeamMemberServices` and dismissal throug
 - [DELETE /locations/{location_id}/team_members/{team_member_id}](https://developer.alteg.io/en/b2b-v3/openapi/team-members/deleteteammember.md): Removes a Team Member from the Location.
 - [GET /locations/{location_id}/team_members/{team_member_id}/schedule](https://developer.alteg.io/en/b2b-v3/openapi/team-members/getteammemberschedule.md): Returns the **configured working schedule** of a Team Member for a date range — the days and working slots set by the business. This is not availability: free bookable slots come from `GET /locations/
 - [PATCH /locations/{location_id}/team_members/{team_member_id}/schedule](https://developer.alteg.io/en/b2b-v3/openapi/team-members/updateteammemberschedule.md): Updates the working schedule as a **merge-patch map keyed by date**: each key is a `YYYY-MM-DD` date, the value sets the working slots of that day, and `null` clears the day. Dates absent from the bod
 - [POST /locations/{location_id}/team_members/{team_member_id}/dismiss](https://developer.alteg.io/en/b2b-v3/openapi/team-members/dismissteammember.md): Dismisses a Team Member as a command: the response is the Team Member with `dismissal_date` set and `is_bookable: false`. Dismissal is not a field of the Team Member card — `updateTeamMember` cannot s
 - [PATCH /locations/{location_id}/team_members/{team_member_id}/services](https://developer.alteg.io/en/b2b-v3/openapi/team-members/updateteammemberservices.md): Updates which services a Team Member provides and the per-member overrides, as a **merge-patch map keyed by `service_id`**: a value creates or updates the link, `null` unlinks the service, services ab
 - [GET /locations/{location_id}/access_roles](https://developer.alteg.io/en/b2b-v3/openapi/team-members/listlocationaccessroles.md): Returns public role ids and titles only; internal permission slugs are never exposed. Cursor-paginated list. Default ordering: title, role_id tie-breaker. Cursors use the `cursor` query parameter, exp
 - [GET /locations/{location_id}/team_members/{team_member_id}/access](https://developer.alteg.io/en/b2b-v3/openapi/team-members/getteammemberaccess.md): Returns current system-access status for a Team Member: `none`, `invited`, or `active`, plus public `role_id` and pending invitation timestamps. The response carries a strong `ETag` for a subsequent a
 - [PATCH /locations/{location_id}/team_members/{team_member_id}/access](https://developer.alteg.io/en/b2b-v3/openapi/team-members/updateteammemberaccess.md): Merge-patch access settings. Only `role_id` is mutable; unavailable roles fail with `422 role_not_assignable`. Requires `If-Match` with the `ETag` from `getTeamMemberAccess` (or a prior successful wri
 - [DELETE /locations/{location_id}/team_members/{team_member_id}/access](https://developer.alteg.io/en/b2b-v3/openapi/team-members/revoketeammemberaccess.md): Revokes pending or active system access and returns the Team Member to `none`; the Team Member record is not deleted. The last location owner cannot be revoked (`409 last_location_owner`). Requires `I
 - [POST /locations/{location_id}/team_members/{team_member_id}/invitation](https://developer.alteg.io/en/b2b-v3/openapi/team-members/sendteammemberinvitation.md): Sends or resends delivery for the single pending invitation of this Team Member. Delivery contact is set only at Team Member create via write-only `access_invitation.phone` / `access_invitation.email`
## Positions

 - [GET /locations/{location_id}/positions](https://developer.alteg.io/en/b2b-v3/openapi/positions/listpositions.md): Returns the position directory of the Location. Reference a position from a Team Member via `position_id`. Cursor-paginated list. Default ordering: weight desc, title, id tie-breaker. Cursors use the
 - [POST /locations/{location_id}/positions](https://developer.alteg.io/en/b2b-v3/openapi/positions/createposition.md): Creates a position.
 - [GET /locations/{location_id}/positions/{position_id}](https://developer.alteg.io/en/b2b-v3/openapi/positions/getposition.md): Returns a single position.
 - [PATCH /locations/{location_id}/positions/{position_id}](https://developer.alteg.io/en/b2b-v3/openapi/positions/updateposition.md): Partially updates a position with a merge-patch body.
 - [DELETE /locations/{location_id}/positions/{position_id}](https://developer.alteg.io/en/b2b-v3/openapi/positions/deleteposition.md): Deletes a position.
## Clients

 - [GET /locations/{location_id}/clients](https://developer.alteg.io/en/b2b-v3/openapi/clients/listclients.md): Searches the client base of the Location. Exact `phone` and `email` contact searches require `clients:read_contact` plus the subject contact permission; without that scope the API returns `403 missing
 - [POST /locations/{location_id}/clients](https://developer.alteg.io/en/b2b-v3/openapi/clients/createclient.md): Creates a client. `name` and `phone` are required. The phone number is deduplicated within the Location: if another (non-deleted) client already owns it, the request fails with `422 phone_taken`. That
 - [GET /locations/{location_id}/clients/{client_id}](https://developer.alteg.io/en/b2b-v3/openapi/clients/getclient.md): Returns a single client. Contact fields (`phone`, `email`, `additional_phone`) require `clients:read_contact` plus the subject's contact permission — otherwise they are returned as `null` and listed i
 - [PATCH /locations/{location_id}/clients/{client_id}](https://developer.alteg.io/en/b2b-v3/openapi/clients/updateclient.md): Partially updates a client with a merge-patch body. Changing the phone to a number owned by another client fails with `422 phone_taken` — the same `clients:write` signal as on create (audited and rate
 - [DELETE /locations/{location_id}/clients/{client_id}](https://developer.alteg.io/en/b2b-v3/openapi/clients/deleteclient.md): Deletes a client (soft delete). A client with the same phone can be created again afterwards.
## Availability

 - [GET /locations/{location_id}/availability](https://developer.alteg.io/en/b2b-v3/openapi/availability/getavailability.md): Computes available start times for the given services within a date range — optionally narrowed to one Team Member. Slots are derived from configured schedules, existing appointments, and service dura
## Resources

 - [GET /locations/{location_id}/resources](https://developer.alteg.io/en/b2b-v3/openapi/resources/listresources.md): Returns the bookable resources of the Location (rooms, chairs, equipment) with their instances. Pass instance ids as `resource_instance_ids` when creating an appointment for services that require a re
## Appointments

 - [POST /locations/{location_id}/appointments](https://developer.alteg.io/en/b2b-v3/openapi/appointments/createappointment.md): Creates an appointment for a client with a Team Member at a chosen start time. - **A client is optional.** Pass `client_id` of an existing client, an inline `client` object resolved attach-only by nor
 - [GET /locations/{location_id}/appointments](https://developer.alteg.io/en/b2b-v3/openapi/appointments/listappointments.md): Returns the appointments of the Location as a cursor-paginated list, newest window first by start time with a stable tie-breaker. Client contact data inside `expand[]=client` uses redacted contact fie
 - [GET /locations/{location_id}/appointments/{appointment_id}](https://developer.alteg.io/en/b2b-v3/openapi/appointments/getappointment.md): Returns a single appointment. `visit_id` links to the billing visit (it is `null` only on appointments created without a client). Re-read the appointment to obtain a fresh `visit_id` — visit grouping
 - [PATCH /locations/{location_id}/appointments/{appointment_id}](https://developer.alteg.io/en/b2b-v3/openapi/appointments/updateappointment.md): Edits or reschedules an appointment with a merge-patch body: start time, Team Member, services, duration, comment. The appointment status is **not** editable here — use the `status` command. Cancellat
 - [POST /locations/{location_id}/appointments/{appointment_id}/cancel](https://developer.alteg.io/en/b2b-v3/openapi/appointments/cancelappointment.md): Cancels an appointment as a command: the response is the appointment with `status: "cancelled"` and `cancelled_at` set. Optionally stores a `reason` and whether to notify the client. Requires `Idempot
 - [POST /locations/{location_id}/appointments/{appointment_id}/status](https://developer.alteg.io/en/b2b-v3/openapi/appointments/setappointmentstatus.md): Moves an appointment through its status lifecycle: `waiting` → `confirmed` → `arrived` / `no_show`. Marking a past visit as `arrived` finalizes it when nothing is left to pay (a fully paid or free vis
## Visits

 - [GET /locations/{location_id}/appointments/{appointment_id}/visit](https://developer.alteg.io/en/b2b-v3/openapi/visits/getvisit.md): Returns the billing state resolved from `appointment_id`: service and products, payment methods applicable to this visit, payments from money and loyalty sources, and `amount_to_pay_minor`. `visit_id`
 - [POST /locations/{location_id}/appointments/{appointment_id}/visit/items](https://developer.alteg.io/en/b2b-v3/openapi/visits/addvisititem.md): Adds one product line to the visit resolved from `appointment_id` — the "came for a haircut, bought shampoo, one receipt" case. Each call appends a single line and never replaces the existing ones, so
## Payments

 - [GET /locations/{location_id}/payment_methods](https://developer.alteg.io/en/b2b-v3/openapi/payments/listpaymentmethods.md): Returns the payment methods configured for the Location - what the business accepts and how each method settles. This is the only source of `payment_method_id` for `createPayment`. It does not say whe
 - [GET /locations/{location_id}/accounts](https://developer.alteg.io/en/b2b-v3/openapi/payments/listaccounts.md): Returns the cash/settlement accounts of the Location. Use an account's `id` as `account_id` in `createPayment` — either as the target of an `account` payment or as an override of a payment method's de
 - [GET /locations/{location_id}/appointments/{appointment_id}/payment_methods/search](https://developer.alteg.io/en/b2b-v3/openapi/payments/searchloyaltyinstrument.md): Looks up a gift card or membership by its number and reports whether it can pay the visit resolved from `appointment_id`. This is for instruments the client does not hold in Altegio: a gift card recei
 - [POST /locations/{location_id}/appointments/{appointment_id}/payments](https://developer.alteg.io/en/b2b-v3/openapi/payments/createpayment.md): Records a money, deposit, loyalty, gift card, membership, or referral-program payment against the visit resolved from `appointment_id`. This records that payment was received (cash, card terminal, cas
 - [POST /locations/{location_id}/appointments/{appointment_id}/payments/{payment_id}/cancel](https://developer.alteg.io/en/b2b-v3/openapi/payments/cancelpayment.md): Locally reverses one payment action in full for the visit resolved from `appointment_id`. Partial reversal is not part of the contract. This is a local cancellation of posting in Altegio, not a paymen
