# KLIGO API and authorization contract

> Scope update (12 September 2026): `LAUNCH_SCOPE.md` takes precedence. Website first; native app later; all four categories. KLIGO bills subscriptions and paid placements only. Historical buyer-deal checkout, commission, escrow, booking-payment and payout proposals below are excluded from launch. See `MATERIALS_HANDOFF.md` for new/surplus material fields.


> Historical architecture baseline, supplemented on 12 September 2026. Read `HANDOFF_READINESS.md` and `CURRENT_ROUTE_INVENTORY.md` first. The current public profile is `/providers/[slug]`; `/marketplace` redirects to `/`; Projects is retired. Legal drafts now exist. Project subtype proposals and older route counts below are not current launch requirements. Vendor references are candidates, not approved integrations.

Status: implementation-ready contract for the prototype baseline. No endpoint in this document should be described as live until it is implemented, protected, tested and backed by persistent data.

## API shape

Use same-origin route handlers under `/api/v1`. Keep browser pages and components independent from storage and third-party vendor SDKs.

- JSON request and response bodies, except direct object-storage transfers.
- Zod validation at every server boundary.
- Opaque IDs in URLs; slugs are for public discovery only.
- UTC ISO 8601 timestamps.
- Money as integer minor units plus ISO currency, for example `{ "amountMinor": 890000, "currency": "ILS" }`.
- Cursor pagination for activity, messages and moderation queues; page-based pagination may remain for small public catalog views.
- `Idempotency-Key` required for request submission, quote send/acceptance, booking confirmation, payments, refunds, subscription changes and moderation decisions.
- `X-Request-Id` returned on every response and recorded with audit events.
- Mutation responses return the canonical saved record and current version, not a client-composed approximation.

## Standard response and error contract

Successful list:

```json
{
  "data": [],
  "pageInfo": {
    "nextCursor": null,
    "hasMore": false
  },
  "requestId": "req_opaque"
}
```

Successful item:

```json
{
  "data": {},
  "requestId": "req_opaque"
}
```

Error:

```json
{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Review the highlighted fields.",
    "fields": {
      "startAt": "Start time must be before end time."
    }
  },
  "requestId": "req_opaque"
}
```

Stable error codes:

| HTTP | Code | Meaning |
| ---: | --- | --- |
| 400 | `VALIDATION_FAILED` | Invalid or incomplete input |
| 401 | `AUTHENTICATION_REQUIRED` | No valid identity/session |
| 403 | `FORBIDDEN` | Actor is authenticated but lacks role, membership or ownership |
| 404 | `NOT_FOUND` | Resource does not exist or must be hidden from this actor |
| 409 | `VERSION_CONFLICT` | Optimistic concurrency version is stale |
| 409 | `STATE_CONFLICT` | Workflow transition is invalid or availability changed |
| 413 | `UPLOAD_TOO_LARGE` | File exceeds policy |
| 422 | `POLICY_BLOCKED` | Valid request blocked by verification, moderation or business rule |
| 429 | `RATE_LIMITED` | Retry after the returned delay |
| 503 | `DEPENDENCY_UNAVAILABLE` | Required vendor/storage service is temporarily unavailable |

Do not reveal whether another user's private record exists. Return `404 NOT_FOUND` where existence itself is sensitive.

## Actor and authorization model

Every request resolves a server-side actor:

```ts
type Actor = {
  userId: string;
  roles: Array<"customer" | "provider_member" | "provider_admin" | "moderator" | "admin">;
  providerMemberships: Array<{
    providerId: string;
    role: "member" | "manager" | "owner" | "billing";
    status: "active" | "suspended";
  }>;
};
```

The type is descriptive only; implementation belongs in the later backend branch. Identity-provider claims establish identity, not KLIGO business roles. Roles and provider memberships come from D1 on the server.

### Authorization matrix

| Resource/action | Anonymous | Customer owner | Provider member | Provider manager/owner | Moderator | Admin |
| --- | --- | --- | --- | --- | --- | --- |
| Read published catalog/profile | Allow | Allow | Allow | Allow | Allow | Allow |
| Read draft/paused provider listing | Deny | Deny | Assigned organization | Assigned organization | Policy-scoped | Allow |
| Create/edit listing draft | Deny | Deny | Assigned organization | Assigned organization | Deny | Support override with audit |
| Submit/publish listing | Deny | Deny | Submit only | Submit/pause/archive | Review/approve if assigned | Allow |
| Create/read customer request | Deny | Own only | Matched redacted view | Matched redacted view | Policy-scoped | Allow |
| Create/send provider quote | Deny | Deny | Assigned organization | Assigned organization | Deny | Support override with audit |
| View/accept quote | Deny | Request owner only | Sending organization | Sending organization | Policy-scoped | Allow |
| View/update booking | Deny | Booking party | Booking party organization | Booking party organization | Case-scoped | Allow |
| Send/read conversation message | Deny | Participant only | Participant organization | Participant organization | Case/support-scoped | Allow |
| Save public item | Deny | Own collection | Own user collection if enabled | Own user collection if enabled | Own collection | Own collection |
| Submit report/evidence | Deny | Authenticated reporter | Authenticated reporter | Authenticated reporter | Read assigned reports | Allow |
| Decide moderation/verification | Deny | Deny | Deny | Deny | Assigned scope | Allow |
| Manage billing/plan | Deny | Own customer payment context | Read only if granted | Assigned organization | Deny | Support override with audit |
| Export/delete account data | Deny | Own account | Own account | Organization request subject to policy | Deny | Process approved request |

Provider access must be scoped by `provider_memberships.provider_id`; a provider role alone never grants access to every provider record.

## Public marketplace endpoints

| Method and endpoint | Actor | Purpose | Core rules |
| --- | --- | --- | --- |
| `GET /api/v1/categories` | Public | Stable taxonomy | Return active categories and localized labels |
| `GET /api/v1/listings` | Public | Search/filter published supply | Filter `status=published`; expose coarse location only |
| `GET /api/v1/listings/:id` | Public | Canonical listing detail | Resolve by opaque ID internally; public page may use slug |
| `GET /api/v1/providers/:id` | Public | Public provider profile | Return approved public fields and current verification badge state |
| `GET /api/v1/projects` | Public | Published opportunities | Redact private publisher/site information |
| `GET /api/v1/service-areas` | Public | Supported pilot geography | Return stable area IDs, labels and optional polygons |

Recommended listing query fields: `type`, `category`, `area`, `query`, `priceMode`, `minPrice`, `maxPrice`, `operator`, `delivery`, `availabilityFrom`, `availabilityTo`, `sort`, `cursor` or `page`.

## Account and preference endpoints

Credential creation, OTP verification and password recovery belong to the selected identity provider, not custom KLIGO endpoints. KLIGO endpoints begin after verified identity.

| Method and endpoint | Actor | Purpose |
| --- | --- | --- |
| `GET /api/v1/me` | Authenticated | Return user, roles, provider memberships and onboarding status |
| `POST /api/v1/me/bootstrap` | Verified identity | Idempotently create/link the KLIGO user record |
| `PATCH /api/v1/me/profile` | Authenticated | Update display/contact fields allowed by policy |
| `PATCH /api/v1/me/preferences` | Authenticated | Persist locale, theme and notification choices |
| `POST /api/v1/me/consents` | Authenticated | Record acceptance of a specific legal-document version |
| `POST /api/v1/provider-organizations` | Authenticated | Create provider organization during approved onboarding |
| `POST /api/v1/provider-organizations/:id/invitations` | Provider owner | Invite a provider team member |
| `POST /api/v1/provider-invitations/:token/accept` | Authenticated invitee | Join the exact organization after token validation |

## Provider and listing endpoints

| Method and endpoint | Actor | Purpose | Core rules |
| --- | --- | --- | --- |
| `GET /api/v1/provider-organizations/:id` | Organization member | Private organization workspace record | Field-level redaction by membership role |
| `PATCH /api/v1/provider-organizations/:id` | Manager/owner | Update provider profile/billing-safe fields | Legal identity changes may trigger re-verification |
| `GET /api/v1/provider-organizations/:id/listings` | Organization member | List all owned listing states | Include draft/rejected/paused records |
| `POST /api/v1/provider-organizations/:id/listings` | Organization member | Create draft | Never publish directly |
| `GET /api/v1/listings/:id/draft` | Owning organization | Load private editor model | Use optimistic `version` |
| `PATCH /api/v1/listings/:id` | Owning organization | Save draft/update allowed fields | Require `expectedVersion` |
| `POST /api/v1/listings/:id/submit` | Owning organization | Validate and enter review | Reject incomplete media/verification requirements |
| `POST /api/v1/listings/:id/pause` | Provider manager/owner | Remove from discovery | Preserve booking obligations |
| `POST /api/v1/listings/:id/archive` | Provider manager/owner | End listing lifecycle | Soft archive; do not erase audit/history |
| `GET /api/v1/listings/:id/availability` | Owning organization | Read blocks and booking conflicts | Return authoritative normalized ranges |
| `POST /api/v1/listings/:id/availability-blocks` | Owning organization | Add available/unavailable/maintenance capacity | Conflict-check confirmed bookings |
| `DELETE /api/v1/availability-blocks/:id` | Owning organization | Remove a manual block | Deny removal of system booking blocks |

## Upload endpoints

| Method and endpoint | Actor | Purpose | Core rules |
| --- | --- | --- | --- |
| `POST /api/v1/uploads/intents` | Authenticated | Issue one short-lived upload intent | Validate purpose, owner, count, size and MIME allowlist |
| `POST /api/v1/uploads/:id/complete` | Upload owner | Confirm object arrival and checksum | Move to `processing`; access remains quarantined until `ready` |
| `GET /api/v1/uploads/:id` | Authorized owner/reviewer | Read processing status/metadata | Never expose raw private object key |
| `DELETE /api/v1/uploads/:id` | Authorized owner | Delete uncommitted or policy-allowed object | Retain required audit record |
| `GET /api/v1/files/:id/access` | Authorized actor | Issue short-lived download/view URL | Check resource-level ACL on every request |

Upload purpose is mandatory: `listing_media`, `request_attachment`, `verification_document`, `report_evidence`, `message_attachment` or `invoice_export`. Public derivatives are created only after scan/moderation approval.

## Customer request and matching endpoints

| Method and endpoint | Actor | Purpose | Core rules |
| --- | --- | --- | --- |
| `GET /api/v1/requests` | Customer | List own requests | Provider actors use a separate matched view |
| `POST /api/v1/requests` | Customer | Create a persistent draft | Accept source listing/provider context |
| `GET /api/v1/requests/:id` | Owner or matched provider | Read authorized view | Redact exact site/contact data for providers until policy stage |
| `PATCH /api/v1/requests/:id` | Customer owner | Save draft | Draft only; require `expectedVersion` |
| `POST /api/v1/requests/:id/submit` | Customer owner | Submit and trigger matching | Transaction: request + items + attachments + event |
| `POST /api/v1/requests/:id/cancel` | Customer owner | Cancel eligible request | Deny once booking terms prevent it |
| `GET /api/v1/provider-organizations/:id/matches` | Organization member | List eligible matched requests | Filter by organization eligibility and assignment |
| `POST /api/v1/request-matches/:id/decline` | Matched provider | Decline match | Capture structured reason without changing customer request |

Matching eligibility order:

1. Published/eligible category or capability.
2. Provider and listing not suspended.
3. Required verification level met.
4. Service area/location eligible.
5. Requested time not known unavailable.
6. Capacity/lead assignment policy.
7. Relevance score for ordering only.

## Quote endpoints

| Method and endpoint | Actor | Purpose | Core rules |
| --- | --- | --- | --- |
| `GET /api/v1/requests/:id/quotes` | Request owner | Compare authorized sent quotes | Exclude provider drafts |
| `POST /api/v1/requests/:id/quotes` | Matched provider | Create quote draft | One provider may have one active quote lineage per request |
| `PATCH /api/v1/quotes/:id` | Sending organization | Update draft | Sent versions are immutable; create revision |
| `POST /api/v1/quotes/:id/send` | Sending organization | Freeze and send a version | Recalculate totals server-side; require validity and availability promise |
| `POST /api/v1/quotes/:id/withdraw` | Sending organization | Withdraw eligible quote | Preserve sent version and reason |
| `POST /api/v1/quotes/:id/decline` | Request owner | Decline quote | Idempotent terminal transition |
| `POST /api/v1/quotes/:id/accept` | Request owner | Accept and create booking draft | Transactionally lock winner and expire competing quotes |

## Booking and payment endpoints

| Method and endpoint | Actor | Purpose | Core rules |
| --- | --- | --- | --- |
| `GET /api/v1/bookings` | Customer/provider party | List authorized bookings | Role-appropriate field projection |
| `GET /api/v1/bookings/:id` | Booking party or case-scoped staff | Booking detail/timeline | Return accepted snapshots, not mutable listing values |
| `POST /api/v1/bookings/:id/provider-confirm` | Provider party | Confirm final availability/scope | Conflict-check again |
| `POST /api/v1/bookings/:id/payment-intent` | Customer owner | Create PSP payment step when approved | Idempotent; amount comes from booking snapshot |
| `POST /api/v1/bookings/:id/cancel` | Eligible booking party | Request/perform cancellation | Apply versioned policy and quote consequences |
| `POST /api/v1/bookings/:id/complete` | Authorized workflow actor | Mark operational completion | Do not auto-release money unless approved policy says so |
| `GET /api/v1/payments/:id` | Payment/booking owner | Display safe payment status | No raw financial credentials |
| `POST /api/v1/payments/:id/refunds` | Authorized staff/workflow | Create refund | Require reason, amount validation and idempotency |
| `POST /api/v1/webhooks/payments/:provider` | Verified PSP signature | Receive authoritative PSP events | Store/deduplicate event before processing |

## Messaging and saved-item endpoints

| Method and endpoint | Actor | Purpose | Core rules |
| --- | --- | --- | --- |
| `GET /api/v1/conversations` | Authenticated | List conversations in which actor participates | Cursor pagination, unread state |
| `POST /api/v1/conversations` | Authorized context party | Create/get context conversation | Idempotent per context and participant set |
| `GET /api/v1/conversations/:id/messages` | Participant | Read authorized messages | Cursor pagination; private attachment links issued separately |
| `POST /api/v1/conversations/:id/messages` | Participant | Send message | Sanitize, persist, update unread state and enqueue notification |
| `POST /api/v1/conversations/:id/read` | Participant | Advance read cursor | Monotonic timestamp/message cursor |
| `GET /api/v1/saved-items` | Authenticated | List own saved items | Resolve only display-safe target projection |
| `PUT /api/v1/saved-items/:type/:id` | Authenticated | Save target | Idempotent composite key |
| `DELETE /api/v1/saved-items/:type/:id` | Authenticated | Unsave target | Idempotent if already absent |

## Verification, reports and moderation endpoints

| Method and endpoint | Actor | Purpose | Core rules |
| --- | --- | --- | --- |
| `GET /api/v1/verification-cases/:id` | Subject or authorized reviewer | View redacted case state | Subject never sees internal risk notes |
| `POST /api/v1/verification-cases` | Subject | Start verification | One active case per subject/level |
| `POST /api/v1/verification-cases/:id/submit` | Subject | Submit evidence for review | Require completed secure uploads |
| `POST /api/v1/reports` | Authenticated reporter | Create report | Private by default; rate-limit abuse |
| `GET /api/v1/reports/:id` | Reporter, party where policy allows, assigned staff | Read role-specific view | Field-level redaction |
| `POST /api/v1/reports/:id/evidence` | Authorized case participant | Attach completed upload | Preserve checksum and chain of custody |
| `GET /api/v1/admin/moderation/queue` | Moderator/admin | Prioritized work queue | Staff-only; cursor pagination |
| `POST /api/v1/admin/moderation/:targetType/:id/decision` | Assigned moderator/admin | Apply policy decision | Require policy code/version, reason and idempotency |
| `POST /api/v1/admin/reports/:id/assign` | Moderator/admin | Assign case | Record prior/new assignee |
| `POST /api/v1/admin/reports/:id/resolve` | Assigned moderator/admin | Resolve case | Notify parties with redacted decision |
| `POST /api/v1/reports/:id/appeals` | Eligible affected party | Appeal decision | New linked review; never overwrite original decision |

## Provider plans, billing and promotion endpoints

| Method and endpoint | Actor | Purpose | Core rules |
| --- | --- | --- | --- |
| `GET /api/v1/plans` | Public/provider | Active plan and entitlement catalog | Versioned prices/terms |
| `GET /api/v1/provider-organizations/:id/subscription` | Organization member | Current plan and limits | Payment-safe projection |
| `POST /api/v1/provider-organizations/:id/subscription-checkout` | Provider owner | Start approved PSP checkout | Entitlement remains pending until webhook |
| `POST /api/v1/provider-organizations/:id/subscription-cancel` | Provider owner | Schedule/carry out cancellation | Apply contract terms and preserve audit history |
| `GET /api/v1/provider-organizations/:id/invoices` | Billing-authorized member | List invoice metadata | Downloads use file access endpoint |
| `POST /api/v1/provider-organizations/:id/campaigns` | Provider manager/owner | Create promotion draft | Validate subject ownership and placement eligibility |
| `POST /api/v1/campaigns/:id/checkout` | Provider owner | Pay for approved campaign | Campaign stays pending until webhook |
| `POST /api/v1/campaigns/:id/stop` | Provider manager/owner | Stop future placement | Preserve spend and attribution records |

## Optimistic concurrency

Mutable aggregate responses include an integer `version`. Update requests pass `expectedVersion`. The server updates only when it matches and returns `409 VERSION_CONFLICT` otherwise. Apply this to profiles, listing drafts, requests, quote drafts, availability blocks, booking operations, reports and moderation cases.

Financial and workflow events are append-only. They are not edited through generic `PATCH` endpoints.

## Event and notification contract

Persist a domain event in the same D1 batch/transactional boundary as the state change where possible.

Minimum event fields:

- `id`, `type`, `aggregate_type`, `aggregate_id`, `aggregate_version`;
- `actor_type`, `actor_id`;
- `occurred_at`, `request_id`, optional `idempotency_key`;
- privacy-safe JSON metadata.

Initial event names:

- `user.onboarded`, `provider.created`, `provider.verification_changed`;
- `listing.submitted`, `listing.published`, `listing.paused`;
- `request.submitted`, `request.matched`, `request.cancelled`;
- `quote.sent`, `quote.accepted`, `quote.declined`, `quote.expired`;
- `booking.created`, `booking.confirmed`, `booking.cancelled`, `booking.completed`;
- `payment.authorized`, `payment.captured`, `payment.failed`, `payment.refunded`;
- `message.sent`, `report.submitted`, `moderation.decided`;
- `subscription.changed`, `campaign.activated`, `campaign.stopped`.

Notification delivery consumes committed events and records channel, recipient, template version, provider reference, attempt count and outcome. UI analytics must never be the source of business truth.

## Rate-limit priorities

Apply the strongest controls to identity/OTP, account bootstrap, uploads, messages, request submission, quote submission, report intake, payment-intent creation and public search scraping. Limits must combine user/account, IP/device signal and resource scopes without blocking legitimate provider teams behind shared networks.

## Implementation order

1. Actor resolution and authorization helpers.
2. `/me`, roles and provider memberships.
3. Listing read/write repository and published catalog adapter.
4. Upload intents and private file access.
5. Requests and deterministic matching.
6. Quote versions, acceptance transaction and bookings.
7. Conversations/messages and notification delivery.
8. Verification, reports and `/admin/*` moderation.
9. PSP billing and booking payment only after legal/vendor approval.

This order lets implementation branches replace demo adapters feature by feature without redesigning pages or creating a second application architecture.
