# API Keys & Webhooks

Manage secret/publishable API keys, webhook endpoints and deliveries, and the public (publishable-key-authenticated) registration endpoint used by the embeddable widget.

Generated from `apps/api/openapi.json`. HTML version: https://docs.institflow.com/en/reference/public-api

25 operations.

## GET /api/v1/api-keys

List API keys (cursor-paginated). Never returns the full key value.

- Operation id: `ApiKeysController_list_v1`
- Permission: `integration.manage`
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | string | no |  |
| `limit` | query | integer | no | 1–100 · default: 25 |

### Responses

**200** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |
| `nextCursor` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/api-keys" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/api-keys

Create a public API key. The full key value is returned exactly once — store it now, it cannot be retrieved again.

- Operation id: `ApiKeysController_create_v1`
- Permission: `integration.manage`
- Auth: credential required
- `Idempotency-Key`: required for API-key callers

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `name` | string | yes | 1–100 chars |
| `kind` | enum: "secret" \| "publishable" | no | default: "secret" |
| `permissions` | array<string> | no | default: [] |
| `allowedOrigins` | array<string> | no | default: [] |
| `rateLimitPerMin` | integer \| null | no | 1–1000000 |
| `expiresAt` | string (date-time) | no |  |

```json
{
  "name": "Algebra I",
  "kind": "secret",
  "permissions": [
    "string"
  ],
  "allowedOrigins": [
    "string"
  ],
  "rateLimitPerMin": 1,
  "expiresAt": "2026-03-02T09:00:00.000Z"
}
```

### Responses

**201** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `name` | string | yes | ≥1 chars |
| `kind` | enum: "secret" \| "publishable" | yes |  |
| `keyPrefix` | string | yes | ≥1 chars |
| `permissions` | array<string> | yes |  |
| `allowedOrigins` | array<string> | yes |  |
| `rateLimitPerMin` | integer \| null | yes | ≤9007199254740991 |
| `effectiveRateLimitPerMin` | integer | yes | 0–9007199254740991 |
| `lastUsedAt` | string (date-time) \| null | yes | ≥1 chars |
| `expiresAt` | string (date-time) \| null | yes | ≥1 chars |
| `revokedAt` | string (date-time) \| null | yes | ≥1 chars |
| `createdAt` | string (date-time) | yes |  |
| `key` | string | yes | ≥1 chars |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/api-keys" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "name": "Algebra I",
    "kind": "secret",
    "permissions": [
      "string"
    ],
    "allowedOrigins": [
      "string"
    ],
    "rateLimitPerMin": 1,
    "expiresAt": "2026-03-02T09:00:00.000Z"
  }'
```

## DELETE /api/v1/api-keys/{id}

Revoke an API key. Idempotent — revoking an already-revoked key is a no-op.

- Operation id: `ApiKeysController_revoke_v1`
- Permission: `integration.manage`
- Auth: credential required
- `Idempotency-Key`: required for API-key callers

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Responses

**200** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `name` | string | yes | ≥1 chars |
| `kind` | enum: "secret" \| "publishable" | yes |  |
| `keyPrefix` | string | yes | ≥1 chars |
| `permissions` | array<string> | yes |  |
| `allowedOrigins` | array<string> | yes |  |
| `rateLimitPerMin` | integer \| null | yes | ≤9007199254740991 |
| `effectiveRateLimitPerMin` | integer | yes | 0–9007199254740991 |
| `lastUsedAt` | string (date-time) \| null | yes | ≥1 chars |
| `expiresAt` | string (date-time) \| null | yes | ≥1 chars |
| `revokedAt` | string (date-time) \| null | yes | ≥1 chars |
| `createdAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X DELETE "https://your-institute.example/api/v1/api-keys/37386ae0-3738-7738-8386-37386ae03738" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## PATCH /api/v1/api-keys/{id}

Change a key’s per-minute rate limit. `null` restores the plan/global default; a value above the plan’s ceiling is rejected with 422. Takes effect immediately — the auth cache is evicted.

- Operation id: `ApiKeysController_updateRateLimit_v1`
- Permission: `integration.manage`
- Auth: credential required
- `Idempotency-Key`: required for API-key callers

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `rateLimitPerMin` | integer \| null | yes | 1–1000000 |

```json
{
  "rateLimitPerMin": 1
}
```

### Responses

**200** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `name` | string | yes | ≥1 chars |
| `kind` | enum: "secret" \| "publishable" | yes |  |
| `keyPrefix` | string | yes | ≥1 chars |
| `permissions` | array<string> | yes |  |
| `allowedOrigins` | array<string> | yes |  |
| `rateLimitPerMin` | integer \| null | yes | ≤9007199254740991 |
| `effectiveRateLimitPerMin` | integer | yes | 0–9007199254740991 |
| `lastUsedAt` | string (date-time) \| null | yes | ≥1 chars |
| `expiresAt` | string (date-time) \| null | yes | ≥1 chars |
| `revokedAt` | string (date-time) \| null | yes | ≥1 chars |
| `createdAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X PATCH "https://your-institute.example/api/v1/api-keys/37386ae0-3738-7738-8386-37386ae03738" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "rateLimitPerMin": 1
  }'
```

## GET /api/v1/leads

List leads captured by the public surface (cursor-paginated), filterable by `status`, `source` and `courseId`.

- Operation id: `LeadsController_list_v1`
- Permission: `students.read`
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | string | no |  |
| `limit` | query | integer | no | 1–100 · default: 25 |
| `status` | query | enum: "new" \| "contacted" \| "converted" \| "discarded" \| "rejected" | no |  |
| `source` | query | enum: "widget" \| "api" \| "join_link" | no |  |
| `courseId` | query | string (uuid) | no |  |

### Responses

**200** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |
| `nextCursor` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/leads" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/leads/{id}

Get one lead by id, including the submitter’s message and contact details.

- Operation id: `LeadsController_getById_v1`
- Permission: `students.read`
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Responses

**200** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `source` | enum: "widget" \| "api" \| "join_link" | yes |  |
| `courseId` | string (uuid) \| null | yes |  |
| `groupId` | string (uuid) \| null | yes |  |
| `fullName` | string | yes |  |
| `phone` | string \| null | yes |  |
| `email` | string \| null | yes |  |
| `message` | string \| null | yes |  |
| `locale` | enum: "ar" \| "en" \| "he" | yes |  |
| `status` | enum: "new" \| "contacted" \| "converted" \| "discarded" \| "rejected" | yes |  |
| `convertedStudentId` | string (uuid) \| null | yes |  |
| `joinLinkLabel` | string \| null | yes |  |
| `suggestedStudentId` | string (uuid) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/leads/37386ae0-3738-7738-8386-37386ae03738" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## PATCH /api/v1/leads/{id}

Change a lead’s status (`new`, `contacted`, `discarded`). `converted` is reachable only through `POST /leads/:id/convert`, which creates the student it points at.

- Operation id: `LeadsController_patch_v1`
- Permission: `students.write`
- Auth: credential required
- `Idempotency-Key`: required for API-key callers

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `status` | enum: "new" \| "contacted" \| "discarded" | yes |  |

```json
{
  "status": "new"
}
```

### Responses

**200** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `source` | enum: "widget" \| "api" \| "join_link" | yes |  |
| `courseId` | string (uuid) \| null | yes |  |
| `groupId` | string (uuid) \| null | yes |  |
| `fullName` | string | yes |  |
| `phone` | string \| null | yes |  |
| `email` | string \| null | yes |  |
| `message` | string \| null | yes |  |
| `locale` | enum: "ar" \| "en" \| "he" | yes |  |
| `status` | enum: "new" \| "contacted" \| "converted" \| "discarded" \| "rejected" | yes |  |
| `convertedStudentId` | string (uuid) \| null | yes |  |
| `joinLinkLabel` | string \| null | yes |  |
| `suggestedStudentId` | string (uuid) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X PATCH "https://your-institute.example/api/v1/leads/37386ae0-3738-7738-8386-37386ae03738" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "status": "new"
  }'
```

## POST /api/v1/leads/{id}/accept

Approve a join request: create a student from the account (`mode: new`, with branch and name) or link it to an existing student (`mode: link`), link the account, enrol into the join link's group when it named one, and tell the control plane. Enrolment problems are reported, not raised. Only a lead from a join link; 409 once decided.

- Operation id: `JoinRequestsController_accept_v1`
- Permission: `students.write`
- Auth: credential required
- `Idempotency-Key`: required for API-key callers

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Request body

```json
"value"
```

### Responses

**201** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `lead` | object | yes |  |
| `studentId` | string (uuid) | yes |  |
| `enrollment` | enum: "enrolled" \| "none" \| "skipped" | yes |  |
| `enrollmentProblem` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/leads/37386ae0-3738-7738-8386-37386ae03738/accept" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '"value"'
```

## POST /api/v1/leads/{id}/convert

Promote a lead to a real student. Creates the student through the normal students path (student-number allocation, audit, `student.created`), then links it back as the lead’s `convertedStudentId`. `firstName`/`lastName` default to a split of the submitted full name.

- Operation id: `LeadsController_convert_v1`
- Permission: `students.write`
- Auth: credential required
- `Idempotency-Key`: required for API-key callers

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `branchId` | string (uuid) | yes |  |
| `firstName` | string | no | 1–100 chars |
| `lastName` | string | no | 1–100 chars |

```json
{
  "branchId": "390b8975-390b-790b-80b8-390b8975390b",
  "firstName": "Ada",
  "lastName": "Lovelace"
}
```

### Responses

**201** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `source` | enum: "widget" \| "api" \| "join_link" | yes |  |
| `courseId` | string (uuid) \| null | yes |  |
| `groupId` | string (uuid) \| null | yes |  |
| `fullName` | string | yes |  |
| `phone` | string \| null | yes |  |
| `email` | string \| null | yes |  |
| `message` | string \| null | yes |  |
| `locale` | enum: "ar" \| "en" \| "he" | yes |  |
| `status` | enum: "new" \| "contacted" \| "converted" \| "discarded" \| "rejected" | yes |  |
| `convertedStudentId` | string (uuid) \| null | yes |  |
| `joinLinkLabel` | string \| null | yes |  |
| `suggestedStudentId` | string (uuid) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/leads/37386ae0-3738-7738-8386-37386ae03738/convert" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "branchId": "390b8975-390b-790b-80b8-390b8975390b",
    "firstName": "Ada",
    "lastName": "Lovelace"
  }'
```

## POST /api/v1/leads/{id}/reject

Reject a join request. The lead is kept as `rejected` for the record; the person sees the decision when they open the link again. 409 once decided.

- Operation id: `JoinRequestsController_reject_v1`
- Permission: `students.write`
- Auth: credential required
- `Idempotency-Key`: required for API-key callers

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Responses

**201** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `source` | enum: "widget" \| "api" \| "join_link" | yes |  |
| `courseId` | string (uuid) \| null | yes |  |
| `groupId` | string (uuid) \| null | yes |  |
| `fullName` | string | yes |  |
| `phone` | string \| null | yes |  |
| `email` | string \| null | yes |  |
| `message` | string \| null | yes |  |
| `locale` | enum: "ar" \| "en" \| "he" | yes |  |
| `status` | enum: "new" \| "contacted" \| "converted" \| "discarded" \| "rejected" | yes |  |
| `convertedStudentId` | string (uuid) \| null | yes |  |
| `joinLinkLabel` | string \| null | yes |  |
| `suggestedStudentId` | string (uuid) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/leads/37386ae0-3738-7738-8386-37386ae03738/reject" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/public/branches

List the institute’s active branches (name, address, phone) for a “where we are” page. Auth: publishable key via `X-Api-Key`.

- Operation id: `PublicCatalogueController_listBranches_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | string | no |  |
| `limit` | query | integer | no | 1–100 · default: 25 |

### Responses

**200** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |
| `nextCursor` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/public/branches" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/public/courses

List the institute’s public courses (cursor-paginated), filterable by `category`, `level` and `language`. Auth: publishable key via `X-Api-Key`. Returns 404 when the institute has not enabled its public catalogue.

- Operation id: `PublicCatalogueController_listCourses_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | string | no |  |
| `limit` | query | integer | no | 1–100 · default: 25 |
| `category` | query | string | no | 1–200 chars |
| `level` | query | string | no | 1–60 chars |
| `language` | query | string | no | 1–60 chars |

### Responses

**200** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |
| `nextCursor` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/public/courses" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/public/courses/{id}

One public course plus its open groups (schedule pattern, start date, and a bucketed `seatsRemaining` hint — never an exact count, never a teacher name). A course that is not public returns the same 404 as one that does not exist.

- Operation id: `PublicCatalogueController_getCourseById_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Responses

**200** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `name` | string | yes |  |
| `description` | string \| null | yes |  |
| `category` | string \| null | yes |  |
| `level` | string \| null | yes |  |
| `language` | string \| null | yes |  |
| `price` | object \| null | yes |  |
| `isActive` | boolean | yes |  |
| `groups` | array<object> | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/public/courses/37386ae0-3738-7738-8386-37386ae03738" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/public/leads

Submit a contact-form lead. Writes a `leads` row, never a student — a staff member promotes it with `POST /leads/:id/convert`. Auth: publishable key via `X-Api-Key` header or `apiKey` body field. Requires `captchaToken` when the institute has configured a CAPTCHA site key.

- Operation id: `PublicLeadsController_create_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `apiKey` | string | no | ≥1 chars |
| `fullName` | string | yes | 1–200 chars |
| `phone` | string | no |  |
| `email` | string (email) | no | 0–320 chars |
| `message` | string | no | 1–2000 chars |
| `courseId` | string (uuid) | no |  |
| `groupId` | string (uuid) | no |  |
| `locale` | enum: "ar" \| "en" \| "he" | no | default: "ar" |
| `captchaToken` | string | no | 1–4000 chars |

```json
{
  "apiKey": "string",
  "fullName": "Ada Lovelace",
  "phone": "+15551234567",
  "email": "ada.lovelace@example.com",
  "message": "string",
  "courseId": "d0e4743a-d0e4-70e4-8e47-d0e4743ad0e4",
  "groupId": "6dc7fce6-6dc7-7dc7-8c7f-6dc7fce66dc7",
  "locale": "ar",
  "captchaToken": "string"
}
```

### Responses

**201** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `ok` | enum: true | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/public/leads" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "apiKey": "string",
    "fullName": "Ada Lovelace",
    "phone": "+15551234567",
    "email": "ada.lovelace@example.com",
    "message": "string",
    "courseId": "d0e4743a-d0e4-70e4-8e47-d0e4743ad0e4",
    "groupId": "6dc7fce6-6dc7-7dc7-8c7f-6dc7fce66dc7",
    "locale": "ar",
    "captchaToken": "string"
  }'
```

## POST /api/v1/public/registrations

Create a lead student + guardian from the embeddable widget. Auth: publishable key via `X-Api-Key` header or `apiKey` body field. An institute that has switched `integration.registrationsToLeads` on gets a `leads` row instead, and the response carries `leadId` with `studentId`/`guardianId` null.

- Operation id: `PublicRegistrationsController_register_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `apiKey` | string | no | ≥1 chars |
| `student` | object | yes |  |
| `guardian` | object | yes |  |
| `courseInterest` | string | no | 1–200 chars |
| `courseId` | string (uuid) | no |  |
| `groupId` | string (uuid) | no |  |
| `captchaToken` | string | no | 1–4000 chars |
| `locale` | enum: "ar" \| "en" \| "he" | no |  |

```json
{
  "apiKey": "string",
  "student": {
    "firstName": "Ada",
    "lastName": "Lovelace",
    "phone": "+15551234567",
    "email": "ada.lovelace@example.com"
  },
  "guardian": {
    "firstName": "Ada",
    "lastName": "Lovelace",
    "phone": "+15551234567",
    "email": "ada.lovelace@example.com"
  },
  "courseInterest": "string",
  "courseId": "d0e4743a-d0e4-70e4-8e47-d0e4743ad0e4",
  "groupId": "6dc7fce6-6dc7-7dc7-8c7f-6dc7fce66dc7",
  "captchaToken": "string",
  "locale": "ar"
}
```

### Responses

**201** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `ok` | enum: true | yes |  |
| `studentId` | string (uuid) \| null | yes |  |
| `guardianId` | string (uuid) \| null | yes |  |
| `leadId` | string (uuid) \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/public/registrations" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "apiKey": "string",
    "student": {
      "firstName": "Ada",
      "lastName": "Lovelace",
      "phone": "+15551234567",
      "email": "ada.lovelace@example.com"
    },
    "guardian": {
      "firstName": "Ada",
      "lastName": "Lovelace",
      "phone": "+15551234567",
      "email": "ada.lovelace@example.com"
    },
    "courseInterest": "string",
    "courseId": "d0e4743a-d0e4-70e4-8e47-d0e4743ad0e4",
    "groupId": "6dc7fce6-6dc7-7dc7-8c7f-6dc7fce66dc7",
    "captchaToken": "string",
    "locale": "ar"
  }'
```

## GET /api/v1/webhooks

List webhook endpoints (cursor-paginated). Never returns the signing secret.

- Operation id: `WebhooksController_list_v1`
- Permission: `integration.manage`
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | string | no |  |
| `limit` | query | integer | no | 1–100 · default: 25 |

### Responses

**200** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |
| `nextCursor` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/webhooks" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/webhooks

Register a webhook endpoint. The signing secret is returned exactly once.

- Operation id: `WebhooksController_create_v1`
- Permission: `integration.manage`
- Auth: credential required
- `Idempotency-Key`: required for API-key callers

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `url` | string | yes |  |
| `events` | array<enum: "student.created" \| "student.updated" \| "student.status_changed" \| "enrollment.created" \| "enrollment.dropped" \| "session.cancelled" \| "attendance.marked" \| "student.absent" \| "exam.published" \| "progress_report.sent" \| "invoice.issued" \| "invoice.overdue" \| "invoice.reminder_requested" \| "payment.recorded" \| "billing.payment_proof.submitted" \| "billing.payment_proof.reviewed" \| "member.invited" \| "lesson.published" \| "lesson.completed" \| "course.completed" \| "course_enrollment.created" \| "course_enrollment.dropped" \| "lesson_file.added" \| "lesson_file.updated" \| "lesson_file.reordered" \| "lesson_file.removed" \| "lesson.audience_changed" \| "lesson_task.saved" \| "lesson_task.removed" \| "reminder.dispatched" \| "notification_type_preference.changed" \| "web_push.subscribed" \| "web_push.unsubscribed" \| "notification_rule.updated" \| "notification_rule.reset" \| "item_reminder_mode.changed" \| "assignment.published" \| "assignment.submitted" \| "assignment.graded" \| "quiz.published" \| "quiz.submitted" \| "quiz.graded" \| "subscription.expiring" \| "subscription.expired" \| "tenant.locked" \| "tenant.unlocked" \| "license.signal" \| "install.update_failed" \| "install.updater_key_bound" \| "relay.install_rekeyed" \| "lead.created" \| "join_link.created" \| "join_link.status_changed" \| "join_link.redeemed" \| "join_request.approved" \| "join_request.rejected" \| "portal_invite.declined" \| "webhook_endpoint.secret_rotated" \| "support.report_received" \| "support.report_status_changed" \| "support.report_replied" \| "support_submission.created" \| "support_submission.status_changed" \| "relay.install_updater_key_bound"> | yes |  |

```json
{
  "url": "https://example.com/webhooks/institflow",
  "events": [
    "student.created"
  ]
}
```

### Responses

**201** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `url` | string | yes | ≥1 chars |
| `events` | array<string> | yes |  |
| `isActive` | boolean | yes |  |
| `failureCount` | integer | yes | 0–9007199254740991 |
| `disabledAt` | string (date-time) \| null | yes | ≥1 chars |
| `previousSecretExpiresAt` | string (date-time) \| null | yes | ≥1 chars |
| `createdAt` | string (date-time) | yes |  |
| `secret` | string | yes | ≥1 chars |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/webhooks" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "url": "https://example.com/webhooks/institflow",
    "events": [
      "student.created"
    ]
  }'
```

## DELETE /api/v1/webhooks/{id}

Delete a webhook endpoint (its delivery history is deleted with it).

- Operation id: `WebhooksController_remove_v1`
- Permission: `integration.manage`
- Auth: credential required
- `Idempotency-Key`: required for API-key callers

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Responses

**200** — The webhook endpoint was deleted.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `ok` | enum: true | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X DELETE "https://your-institute.example/api/v1/webhooks/37386ae0-3738-7738-8386-37386ae03738" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/webhooks/{id}

Get one webhook endpoint by id.

- Operation id: `WebhooksController_getById_v1`
- Permission: `integration.manage`
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Responses

**200** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `url` | string | yes | ≥1 chars |
| `events` | array<string> | yes |  |
| `isActive` | boolean | yes |  |
| `failureCount` | integer | yes | 0–9007199254740991 |
| `disabledAt` | string (date-time) \| null | yes | ≥1 chars |
| `previousSecretExpiresAt` | string (date-time) \| null | yes | ≥1 chars |
| `createdAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/webhooks/37386ae0-3738-7738-8386-37386ae03738" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## PATCH /api/v1/webhooks/{id}

Update a webhook endpoint (url, subscribed events, active state).

- Operation id: `WebhooksController_update_v1`
- Permission: `integration.manage`
- Auth: credential required
- `Idempotency-Key`: required for API-key callers

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `url` | string | no |  |
| `events` | array<enum: "student.created" \| "student.updated" \| "student.status_changed" \| "enrollment.created" \| "enrollment.dropped" \| "session.cancelled" \| "attendance.marked" \| "student.absent" \| "exam.published" \| "progress_report.sent" \| "invoice.issued" \| "invoice.overdue" \| "invoice.reminder_requested" \| "payment.recorded" \| "billing.payment_proof.submitted" \| "billing.payment_proof.reviewed" \| "member.invited" \| "lesson.published" \| "lesson.completed" \| "course.completed" \| "course_enrollment.created" \| "course_enrollment.dropped" \| "lesson_file.added" \| "lesson_file.updated" \| "lesson_file.reordered" \| "lesson_file.removed" \| "lesson.audience_changed" \| "lesson_task.saved" \| "lesson_task.removed" \| "reminder.dispatched" \| "notification_type_preference.changed" \| "web_push.subscribed" \| "web_push.unsubscribed" \| "notification_rule.updated" \| "notification_rule.reset" \| "item_reminder_mode.changed" \| "assignment.published" \| "assignment.submitted" \| "assignment.graded" \| "quiz.published" \| "quiz.submitted" \| "quiz.graded" \| "subscription.expiring" \| "subscription.expired" \| "tenant.locked" \| "tenant.unlocked" \| "license.signal" \| "install.update_failed" \| "install.updater_key_bound" \| "relay.install_rekeyed" \| "lead.created" \| "join_link.created" \| "join_link.status_changed" \| "join_link.redeemed" \| "join_request.approved" \| "join_request.rejected" \| "portal_invite.declined" \| "webhook_endpoint.secret_rotated" \| "support.report_received" \| "support.report_status_changed" \| "support.report_replied" \| "support_submission.created" \| "support_submission.status_changed" \| "relay.install_updater_key_bound"> | no |  |
| `isActive` | boolean | no |  |

```json
{
  "url": "https://example.com/webhooks/institflow",
  "events": [
    "student.created"
  ],
  "isActive": true
}
```

### Responses

**200** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `url` | string | yes | ≥1 chars |
| `events` | array<string> | yes |  |
| `isActive` | boolean | yes |  |
| `failureCount` | integer | yes | 0–9007199254740991 |
| `disabledAt` | string (date-time) \| null | yes | ≥1 chars |
| `previousSecretExpiresAt` | string (date-time) \| null | yes | ≥1 chars |
| `createdAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X PATCH "https://your-institute.example/api/v1/webhooks/37386ae0-3738-7738-8386-37386ae03738" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "url": "https://example.com/webhooks/institflow",
    "events": [
      "student.created"
    ],
    "isActive": true
  }'
```

## GET /api/v1/webhooks/{id}/deliveries

List an endpoint’s delivery history (cursor-paginated, optional status filter).

- Operation id: `WebhooksController_listDeliveries_v1`
- Permission: `integration.manage`
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |
| `cursor` | query | string | no |  |
| `limit` | query | integer | no | 1–100 · default: 25 |
| `status` | query | enum: "queued" \| "delivered" \| "failed" \| "disabled" | no |  |

### Responses

**200** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |
| `nextCursor` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/webhooks/37386ae0-3738-7738-8386-37386ae03738/deliveries" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/webhooks/{id}/deliveries/{deliveryId}/replay

Replay a delivery: resets it to `queued` and enqueues an immediate send attempt.

- Operation id: `WebhooksController_replayDelivery_v1`
- Permission: `integration.manage`
- Auth: credential required
- `Idempotency-Key`: required for API-key callers

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |
| `deliveryId` | path | string | yes |  |

### Responses

**201** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `endpointId` | string (uuid) | yes |  |
| `eventId` | string (uuid) | yes |  |
| `eventType` | string | yes | ≥1 chars |
| `status` | enum: "queued" \| "delivered" \| "failed" \| "disabled" | yes |  |
| `attempts` | integer | yes | 0–9007199254740991 |
| `lastStatusCode` | integer \| null | yes | -9007199254740991–9007199254740991 |
| `responseExcerpt` | string \| null | yes | ≥1 chars |
| `nextAttemptAt` | string (date-time) \| null | yes | ≥1 chars |
| `deliveredAt` | string (date-time) \| null | yes | ≥1 chars |
| `createdAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/webhooks/37386ae0-3738-7738-8386-37386ae03738/deliveries/67cebe50-67ce-77ce-8ceb-67cebe5067ce/replay" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/webhooks/{id}/rotate-secret

Rotate the signing secret. The new secret is returned exactly once; the endpoint keeps its id and delivery history. For `overlapSec` (default 24 h, max 7 days) every delivery carries two `v1=` signatures — old and new — so you can redeploy without dropping an event. Send `overlapSec: 0` to retire the old secret immediately.

- Operation id: `WebhooksController_rotateSecret_v1`
- Permission: `integration.manage`
- Auth: credential required
- `Idempotency-Key`: required for API-key callers

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `overlapSec` | integer | no | 0–604800 |

```json
{
  "overlapSec": 0
}
```

### Responses

**201** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `url` | string | yes | ≥1 chars |
| `events` | array<string> | yes |  |
| `isActive` | boolean | yes |  |
| `failureCount` | integer | yes | 0–9007199254740991 |
| `disabledAt` | string (date-time) \| null | yes | ≥1 chars |
| `previousSecretExpiresAt` | string (date-time) \| null | yes | ≥1 chars |
| `createdAt` | string (date-time) | yes |  |
| `secret` | string | yes | ≥1 chars |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/webhooks/37386ae0-3738-7738-8386-37386ae03738/rotate-secret" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "overlapSec": 0
  }'
```

## POST /api/v1/webhooks/{id}/test

Send a signed test ping to the endpoint immediately (not persisted as a delivery).

- Operation id: `WebhooksController_testPing_v1`
- Permission: `integration.manage`
- Auth: credential required
- `Idempotency-Key`: required for API-key callers

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Responses

**201** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `ok` | boolean | yes |  |
| `statusCode` | integer \| null | yes | -9007199254740991–9007199254740991 |
| `responseExcerpt` | string \| null | yes | ≥1 chars |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/webhooks/37386ae0-3738-7738-8386-37386ae03738/test" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/webhooks/events

The catalogue of subscribable webhook events with each one’s payload fields. No authentication — these are the exact strings `POST /api/v1/webhooks` accepts in `events[]`.

- Operation id: `WebhookEventsController_listEvents_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Responses

**200** — Success.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/webhooks/events" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```
