# Waitlist and access API

> Invite-only access: the form service's sign-up hook, and for the people who run the service, the waitlist and access grants. Operator routes need the operator token.

- [Receive a waitlist sign-up](#receive-a-waitlist-sign-up): `POST /v1/waitlist/tally`
- [List waitlist entries (operators)](#list-waitlist-entries-operators): `GET /v1/operator/waitlist`
- [Approve a waitlist entry (operators)](#approve-a-waitlist-entry-operators): `POST /v1/operator/waitlist/{entry_id}/approve`
- [Reject a waitlist entry (operators)](#reject-a-waitlist-entry-operators): `POST /v1/operator/waitlist/{entry_id}/reject`
- [Revoke a waitlist entry (operators)](#revoke-a-waitlist-entry-operators): `POST /v1/operator/waitlist/{entry_id}/revoke`
- [List access grants (operators)](#list-access-grants-operators): `GET /v1/operator/access-grants`
- [Grant access (operators)](#grant-access-operators): `POST /v1/operator/access-grants`
- [Revoke an access grant (operators)](#revoke-an-access-grant-operators): `POST /v1/operator/access-grants/{grant_id}/revoke`

## Receive a waitlist sign-up

`POST /v1/waitlist/tally`

Accept a waitlist sign-up from the form service (Tally). The raw body
must carry a valid `Tally-Signature` (base64 HMAC-SHA256 under the
configured signing secret); unsigned or mis-signed requests are refused.
Events other than `FORM_RESPONSE` are acknowledged and ignored. A second
sign-up from the same address refreshes its answers.

- **Authentication:** none on this route.
- **Who:** The form service (signed request).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `Tally-Signature` | header | `string` | Yes | Base64 HMAC-SHA256 of the raw body under the waitlist signing secret |

**Request body** (`application/json`)

No fields.

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | — | [`WaitlistAckDto`](/docs/api/schemas#waitlistackdto) |
| `401` | `invalid_signature`: missing or wrong signature | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `404` | the waitlist webhook is not configured | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `422` | the event has no valid email answer | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl -X POST "$VMYARD_API_URL/v1/waitlist/tally" \
  -H "Tally-Signature: $TALLY_SIGNATURE" \
  -H "Content-Type: application/json" \
  -d '"string"'
```

```json title="Response 200"
{
  "received": false
}
```

## List waitlist entries (operators)

`GET /v1/operator/waitlist`

Waitlist entries, newest first.

- **Authentication:** none on this route.
- **Who:** Service operators (operator token).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `status` | query | [`WaitlistFilterDto`](/docs/api/schemas#waitlistfilterdto) | No | `pending` (default), `approved`, `rejected`, `revoked`, or `all`. |
| `limit` | query | `integer` | No | 1-500, default 100; newest first. |
| `X-VMYard-Operator-Token` | header | `string` | Yes | Operator token from the service configuration |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | — | [`WaitlistPage`](/docs/api/schemas#waitlistpage) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `404` | operator access is not configured | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl "$VMYARD_API_URL/v1/operator/waitlist" \
  -H "X-VMYard-Operator-Token: $X_VMYARD_OPERATOR_TOKEN"
```

```json title="Response 200"
{
  "items": [
    {
      "access_grant_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
      "company": "string",
      "created_at": "string",
      "decided_at": "string",
      "decided_by": "string",
      "decision_reason": "string",
      "email": "ada@example.com",
      "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
      "name": "dev",
      "raw_answers": {},
      "status": "pending",
      "updated_at": "string",
      "use_case": "string"
    }
  ]
}
```

## Approve a waitlist entry (operators)

`POST /v1/operator/waitlist/{entry_id}/approve`

Approve an entry: its email gets an access grant (with the initial
credit), and the "you're in" email is sent. Approving an approved entry
changes nothing; a rejected or revoked entry may be approved again.

- **Authentication:** none on this route.
- **Who:** Service operators (operator token).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `entry_id` | path | `string (uuid)` | Yes |  |
| `X-VMYard-Operator-Token` | header | `string` | Yes | Operator token from the service configuration |

**Request body** (`application/json`, [`ApproveWaitlistRequest`](/docs/api/schemas#approvewaitlistrequest))

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `operator` | `string` | Yes | The individual operator acting, 1-200 characters. |
| `initial_credit_micros` | `integer \| null` | No | Credits from VMYard granted once to the person's own organization (now if they already signed in, else at their first sign-in). Defaults to the service's `access.initial_credit_micros`; 0 grants none. |
| `notify` | `boolean \| null` | No | Send the "you're in" email (default true). |
| `reason` | `string \| null` | No | Why, 1-500 characters; optional. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | — | [`WaitlistDecisionDto`](/docs/api/schemas#waitlistdecisiondto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `404` | operator access is not configured or the entry is unknown | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `422` | Invalid request (`invalid_request`); `field_errors` names the fields. | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl -X POST "$VMYARD_API_URL/v1/operator/waitlist/$ENTRY_ID/approve" \
  -H "X-VMYard-Operator-Token: $X_VMYARD_OPERATOR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "operator": "string"
}'
```

```json title="Response 200"
{
  "changed": false,
  "entry": {
    "access_grant_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
    "company": "string",
    "created_at": "string",
    "decided_at": "string",
    "decided_by": "string",
    "decision_reason": "string",
    "email": "ada@example.com",
    "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
    "name": "dev",
    "raw_answers": {},
    "status": "pending",
    "updated_at": "string",
    "use_case": "string"
  },
  "grant": {
    "created_at": "string",
    "credited_at": "string",
    "credited_organization_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
    "email": "ada@example.com",
    "granted_by": "string",
    "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
    "initial_credit_micros": 0,
    "provider": "github",
    "reason": "string",
    "revoke_reason": "string",
    "revoked_at": "string",
    "revoked_by": "string",
    "subject": "string"
  },
  "notification": "sent"
}
```

## Reject a waitlist entry (operators)

`POST /v1/operator/waitlist/{entry_id}/reject`

Reject a pending entry. Rejecting a rejected entry changes nothing; an
approved entry is revoked instead (`409 waitlist_decision_conflict`).

- **Authentication:** none on this route.
- **Who:** Service operators (operator token).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `entry_id` | path | `string (uuid)` | Yes |  |
| `X-VMYard-Operator-Token` | header | `string` | Yes | Operator token from the service configuration |

**Request body** (`application/json`, [`WaitlistDecisionRequest`](/docs/api/schemas#waitlistdecisionrequest))

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `operator` | `string` | Yes | The individual operator acting, 1-200 characters. |
| `reason` | `string` | Yes | Why, 1-500 characters. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | — | [`WaitlistDecisionDto`](/docs/api/schemas#waitlistdecisiondto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `404` | operator access is not configured or the entry is unknown | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `409` | `waitlist_decision_conflict` | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `422` | Invalid request (`invalid_request`); `field_errors` names the fields. | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl -X POST "$VMYARD_API_URL/v1/operator/waitlist/$ENTRY_ID/reject" \
  -H "X-VMYard-Operator-Token: $X_VMYARD_OPERATOR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "operator": "string",
  "reason": "string"
}'
```

```json title="Response 200"
{
  "changed": false,
  "entry": {
    "access_grant_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
    "company": "string",
    "created_at": "string",
    "decided_at": "string",
    "decided_by": "string",
    "decision_reason": "string",
    "email": "ada@example.com",
    "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
    "name": "dev",
    "raw_answers": {},
    "status": "pending",
    "updated_at": "string",
    "use_case": "string"
  },
  "grant": {
    "created_at": "string",
    "credited_at": "string",
    "credited_organization_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
    "email": "ada@example.com",
    "granted_by": "string",
    "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
    "initial_credit_micros": 0,
    "provider": "github",
    "reason": "string",
    "revoke_reason": "string",
    "revoked_at": "string",
    "revoked_by": "string",
    "subject": "string"
  },
  "notification": "sent"
}
```

## Revoke a waitlist entry (operators)

`POST /v1/operator/waitlist/{entry_id}/revoke`

Revoke an approved entry: its grant is revoked, so the person (and the
organizations only they approve) can no longer create organizations,
machines, or API keys. Running machines keep running; suspend the
organization to stop them.

- **Authentication:** none on this route.
- **Who:** Service operators (operator token).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `entry_id` | path | `string (uuid)` | Yes |  |
| `X-VMYard-Operator-Token` | header | `string` | Yes | Operator token from the service configuration |

**Request body** (`application/json`, [`WaitlistDecisionRequest`](/docs/api/schemas#waitlistdecisionrequest))

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `operator` | `string` | Yes | The individual operator acting, 1-200 characters. |
| `reason` | `string` | Yes | Why, 1-500 characters. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | — | [`WaitlistDecisionDto`](/docs/api/schemas#waitlistdecisiondto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `404` | operator access is not configured or the entry is unknown | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `409` | `waitlist_decision_conflict`: the entry is not approved | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `422` | Invalid request (`invalid_request`); `field_errors` names the fields. | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl -X POST "$VMYARD_API_URL/v1/operator/waitlist/$ENTRY_ID/revoke" \
  -H "X-VMYard-Operator-Token: $X_VMYARD_OPERATOR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "operator": "string",
  "reason": "string"
}'
```

```json title="Response 200"
{
  "changed": false,
  "entry": {
    "access_grant_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
    "company": "string",
    "created_at": "string",
    "decided_at": "string",
    "decided_by": "string",
    "decision_reason": "string",
    "email": "ada@example.com",
    "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
    "name": "dev",
    "raw_answers": {},
    "status": "pending",
    "updated_at": "string",
    "use_case": "string"
  },
  "grant": {
    "created_at": "string",
    "credited_at": "string",
    "credited_organization_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
    "email": "ada@example.com",
    "granted_by": "string",
    "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
    "initial_credit_micros": 0,
    "provider": "github",
    "reason": "string",
    "revoke_reason": "string",
    "revoked_at": "string",
    "revoked_by": "string",
    "subject": "string"
  },
  "notification": "sent"
}
```

## List access grants (operators)

`GET /v1/operator/access-grants`

Access grants, newest first.

- **Authentication:** none on this route.
- **Who:** Service operators (operator token).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `include_revoked` | query | `boolean` | No | Include revoked grants (default false). |
| `limit` | query | `integer` | No | 1-500, default 100; newest first. |
| `X-VMYard-Operator-Token` | header | `string` | Yes | Operator token from the service configuration |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | — | [`AccessGrantPage`](/docs/api/schemas#accessgrantpage) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `404` | operator access is not configured | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl "$VMYARD_API_URL/v1/operator/access-grants" \
  -H "X-VMYard-Operator-Token: $X_VMYARD_OPERATOR_TOKEN"
```

```json title="Response 200"
{
  "items": [
    {
      "created_at": "string",
      "credited_at": "string",
      "credited_organization_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
      "email": "ada@example.com",
      "granted_by": "string",
      "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
      "initial_credit_micros": 0,
      "provider": "github",
      "reason": "string",
      "revoke_reason": "string",
      "revoked_at": "string",
      "revoked_by": "string",
      "subject": "string"
    }
  ]
}
```

## Grant access (operators)

`POST /v1/operator/access-grants`

Grant access without a waitlist entry: a verified login email, or one
sign-in identity (`provider` and `subject`, such as an ERP.AI account's
OIDC subject). No email is sent.

- **Authentication:** none on this route.
- **Who:** Service operators (operator token).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-VMYard-Operator-Token` | header | `string` | Yes | Operator token from the service configuration |

**Request body** (`application/json`, [`CreateAccessGrantRequest`](/docs/api/schemas#createaccessgrantrequest))

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `operator` | `string` | Yes | The individual operator acting, 1-200 characters. |
| `email` | `string \| null` | No | Grant a verified login email; or name `provider` and `subject`. |
| `initial_credit_micros` | `integer \| null` | No | As on approval; defaults to the service's initial credit, 0 for none. |
| `provider` | `string \| null` | No | A configured sign-in provider such as `erpai`. |
| `reason` | `string \| null` | No |  |
| `subject` | `string \| null` | No | The provider's subject (`sub`) for the account, 1-255 characters. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | An active grant already named this person | [`AccessGrantCreatedDto`](/docs/api/schemas#accessgrantcreateddto) |
| `201` | — | [`AccessGrantCreatedDto`](/docs/api/schemas#accessgrantcreateddto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `404` | operator access is not configured | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `422` | Invalid request (`invalid_request`); `field_errors` names the fields. | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl -X POST "$VMYARD_API_URL/v1/operator/access-grants" \
  -H "X-VMYard-Operator-Token: $X_VMYARD_OPERATOR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "operator": "string"
}'
```

```json title="Response 200"
{
  "created": false,
  "grant": {
    "created_at": "string",
    "credited_at": "string",
    "credited_organization_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
    "email": "ada@example.com",
    "granted_by": "string",
    "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
    "initial_credit_micros": 0,
    "provider": "github",
    "reason": "string",
    "revoke_reason": "string",
    "revoked_at": "string",
    "revoked_by": "string",
    "subject": "string"
  }
}
```

## Revoke an access grant (operators)

`POST /v1/operator/access-grants/{grant_id}/revoke`

Revoke a grant; a waitlist entry approved through it becomes revoked.
Revoking a revoked grant changes nothing.

- **Authentication:** none on this route.
- **Who:** Service operators (operator token).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `grant_id` | path | `string (uuid)` | Yes |  |
| `X-VMYard-Operator-Token` | header | `string` | Yes | Operator token from the service configuration |

**Request body** (`application/json`, [`WaitlistDecisionRequest`](/docs/api/schemas#waitlistdecisionrequest))

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `operator` | `string` | Yes | The individual operator acting, 1-200 characters. |
| `reason` | `string` | Yes | Why, 1-500 characters. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `200` | — | [`AccessGrantDto`](/docs/api/schemas#accessgrantdto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `404` | operator access is not configured or the grant is unknown | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `422` | Invalid request (`invalid_request`); `field_errors` names the fields. | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl -X POST "$VMYARD_API_URL/v1/operator/access-grants/$GRANT_ID/revoke" \
  -H "X-VMYard-Operator-Token: $X_VMYARD_OPERATOR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "operator": "string",
  "reason": "string"
}'
```

```json title="Response 200"
{
  "created_at": "string",
  "credited_at": "string",
  "credited_organization_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
  "email": "ada@example.com",
  "granted_by": "string",
  "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
  "initial_credit_micros": 0,
  "provider": "github",
  "reason": "string",
  "revoke_reason": "string",
  "revoked_at": "string",
  "revoked_by": "string",
  "subject": "string"
}
```
