{"openapi":"3.1.0","info":{"title":"STRALO API","version":"1.0.0","description":"STRALO REST surface — agents, bookings, proposals. Schemas derived from the live route handlers and src/lib/contracts/*; the document at /openapi.json regenerates on each request."},"servers":[{"url":"https://stralo.polsia.app","description":"STRALO production"}],"tags":[{"name":"Agents","description":"Agent seat issuance (POST /agents creates a child agent)."},{"name":"Bookings","description":"Bookings on the authenticated agentId calendar."},{"name":"Proposals","description":"Slot-transfer proposals between agents."}],"components":{"schemas":{"AgentCreate":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"metadata":{"type":"object","additionalProperties":{}},"config":{"type":"object","additionalProperties":{}},"idempotency_key":{"type":"string","minLength":1,"maxLength":255},"bootstrap":{"type":"boolean","const":true}},"required":["name"],"additionalProperties":false},"Agent":{"type":"object","properties":{"id":{"type":"string","minLength":1},"public_token":{"type":"string","minLength":1},"created_at":{"type":"string","format":"date-time"},"next_step":{"type":"object","properties":{"save_public_token":{"type":"boolean","const":true},"authorization":{"type":"string","const":"Authorization: Bearer <public_token>"},"x_api_key":{"type":"string","const":"X-API-Key: <public_token>"},"message":{"type":"string","minLength":1}},"required":["save_public_token","authorization","x_api_key","message"],"additionalProperties":false}},"required":["id","public_token","created_at","next_step"],"additionalProperties":false},"BookingCreate":{"type":"object","properties":{"agentId":{"type":"string","minLength":1,"maxLength":128},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"}},"required":["agentId","startsAt","endsAt"],"additionalProperties":false},"BookingItem":{"type":"object","properties":{"agentId":{"type":"string","minLength":1,"maxLength":128},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"},"id":{"type":"string"},"status":{"type":"string"},"rrule":{"type":["string","null"]},"warningSentAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time"}},"required":["agentId","startsAt","endsAt","id","status","warningSentAt","createdAt"],"additionalProperties":false},"BookingListEnvelope":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"agentId":{"type":"string","minLength":1,"maxLength":128},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"},"id":{"type":"string"},"status":{"type":"string"},"rrule":{"type":["string","null"]},"warningSentAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time"}},"required":["agentId","startsAt","endsAt","id","status","warningSentAt","createdAt"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false},"BookingListQuery":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":200},"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"}},"additionalProperties":false},"OccurrenceSlot":{"type":"object","properties":{"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"}},"required":["startsAt","endsAt"],"additionalProperties":false},"OccurrencesEnvelope":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"}},"required":["startsAt","endsAt"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false},"OccurrencesQuery":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":200}},"additionalProperties":false},"ProposalCreate":{"type":"object","properties":{"targetAgentId":{"type":"string","minLength":1,"maxLength":128},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"}},"required":["targetAgentId","startsAt","endsAt"],"additionalProperties":false},"ProposalItem":{"type":"object","properties":{"id":{"type":"string"},"proposerAgentId":{"type":"string"},"targetAgentId":{"type":"string"},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"},"status":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}},"required":["id","proposerAgentId","targetAgentId","startsAt","endsAt","status","createdAt"],"additionalProperties":false},"ErrorEnvelope":{"type":"object","properties":{"error":{"type":"string","description":"Machine-readable error code."},"message":{"type":"string","description":"Human-readable explanation."}},"required":["error"]},"ErrorUnauthorized":{"type":"object","properties":{"error":{"type":"string","enum":["unauthorized"]},"message":{"type":"string"}},"required":["error"]},"ErrorValidation":{"type":"object","properties":{"errors":{"type":"object","additionalProperties":{"type":"string"},"description":"Map of field name to first validation message."}},"required":["errors"]},"ErrorForbidden":{"type":"object","properties":{"error":{"type":"string","enum":["forbidden"]},"message":{"type":"string"}},"required":["error"]},"ErrorNotFound":{"type":"object","properties":{"error":{"type":"string","enum":["not_found"]},"message":{"type":"string"}},"required":["error"]},"ErrorConflict":{"type":"object","properties":{"error":{"type":"string","enum":["conflict","bootstrap_claimed","slot_taken","proposal_not_pending","booking_cap_reached"]},"message":{"type":"string"}},"required":["error"]},"ErrorInternal":{"type":"object","properties":{"error":{"type":"string","enum":["Internal Server Error"]}},"required":["error"]}},"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Authorization: Bearer <sk_…> from POST /agents."},"apiKeyHeader":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Alternative to bearerAuth: X-API-Key: <sk_…> from POST /agents."}}},"paths":{"/api/agents":{"post":{"tags":["Agents"],"operationId":"postAgents","summary":"Bootstrap the first agent or create a child agent","description":"Send { bootstrap: true } without credentials to claim the first agent on a brand-new installation. Once an Agent exists, bootstrap is no longer available: normal child-agent creation requires Authorization: Bearer or X-API-Key. Every successful response returns public_token once, plus non-secret next_step guidance; the database stores only its SHA-256 hash.","parameters":[{"name":"Authorization","in":"header","required":false,"schema":{"type":"string"},"description":"Use either this Bearer <sk_…> credential or X-API-Key, not both."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Alternative credential header: X-API-Key: <sk_…> from POST /agents."},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Optional client-supplied idempotency token. Agent creation rejects a reused idempotency_key with 409 conflict."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentCreate"}}}},"responses":{"201":{"description":"Agent created. Body { id, public_token, created_at }. Save the public_token before continuing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Agent"}}}},"400":{"description":"Request body or query failed Zod validation — body is { errors: { field: msg } }.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorValidation"}}}},"401":{"description":"Ordinary creation without a credential returns missing-credential guidance; any supplied invalid credential returns invalid-credential guidance. Only explicit bootstrap:true may omit credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorUnauthorized"}}}},"409":{"description":"Idempotency conflict, or bootstrap_claimed when the first agent already exists. A repeated bootstrap response never includes public_token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorConflict"}}}},"500":{"description":"Unexpected helper failure past the contract guard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorInternal"}}}}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]},{}]}},"/api/bookings":{"post":{"tags":["Bookings"],"operationId":"postBookings","summary":"Confirm a booking on the authenticated agent calendar","description":"Agent credential required. The resolved agentId MUST match the body agentId — a credential for agent A cannot create bookings on behalf of agent B. Overlap rejections (same agentId, overlapping tstzrange) return 409 slot_taken because the database EXCLUDE constraint raises 23P01 at commit time.","parameters":[{"name":"Authorization","in":"header","required":false,"schema":{"type":"string"},"description":"Use either this Bearer <sk_…> credential or X-API-Key, not both."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Alternative credential header: X-API-Key: <sk_…> from POST /agents."},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Optional client-supplied idempotency token. Agent creation rejects a reused idempotency_key with 409 conflict."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingCreate"}}}},"responses":{"201":{"description":"Booking persisted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingItem"}}}},"400":{"description":"Request body or query failed Zod validation — body is { errors: { field: msg } }.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorValidation"}}}},"401":{"description":"Missing or unparseable Authorization or X-API-Key header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorUnauthorized"}}}},"403":{"description":"Credential authenticated but not authorized for this row (mismatched agentId / target). Body { error: \"forbidden\", message }.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorForbidden"}}}},"409":{"description":"Resource conflict (slot overlap, proposal already terminal). Body { error, message }.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorConflict"}}}},"429":{"description":"Free-tier agent cap exceeded.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["booking_cap_reached"]},"message":{"type":"string"}},"required":["error"]}}}},"500":{"description":"Unexpected helper failure past the contract guard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorInternal"}}}}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]},"get":{"tags":["Bookings"],"operationId":"getBookings","summary":"List bookings for the authenticated agent","description":"Returns confirmed AND cancelled rows owned by the authenticated agent, ordered by startsAt ascending. Optional ?from / ?to narrow the result to bookings whose [startsAt, endsAt) window intersects [from, to). Half-open [) boundaries — back-to-back bookings do not collide.","parameters":[{"name":"Authorization","in":"header","required":false,"schema":{"type":"string"},"description":"Use either this Bearer <sk_…> credential or X-API-Key, not both."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Alternative credential header: X-API-Key: <sk_…> from POST /agents."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200},"description":"Cap on returned rows. Default 50."},{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Inclusive lower bound for the [startsAt, endsAt) overlap window. RFC 3339 with offset."},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Exclusive upper bound. RFC 3339 with offset. Cross-field check enforces to >= from."}],"responses":{"200":{"description":"List of bookings.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingListEnvelope"}}}},"400":{"description":"Request body or query failed Zod validation — body is { errors: { field: msg } }.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorValidation"}}}},"401":{"description":"Missing or unparseable Authorization or X-API-Key header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorUnauthorized"}}}},"500":{"description":"Unexpected helper failure past the contract guard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorInternal"}}}}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/api/bookings/{id}":{"delete":{"tags":["Bookings"],"operationId":"deleteBookingsId","summary":"Soft-cancel a booking","description":"Sets status=\"cancelled\" but preserves the audit row — no DELETE on the table. Idempotent: cancelling an already-cancelled id returns 204. The authenticated agent must own the row; cross-agent cancellation is structurally impossible because the UPDATE is keyed by the credential-derived agentId.","parameters":[{"name":"Authorization","in":"header","required":false,"schema":{"type":"string"},"description":"Use either this Bearer <sk_…> credential or X-API-Key, not both."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Alternative credential header: X-API-Key: <sk_…> from POST /agents."},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Optional client-supplied idempotency token. Agent creation rejects a reused idempotency_key with 409 conflict."},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The row id (booking, proposal, etc.) owned by the authenticated agent."}],"responses":{"204":{"description":"Soft-cancel succeeded, or row was already cancelled."},"401":{"description":"Missing or unparseable Authorization or X-API-Key header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorUnauthorized"}}}},"403":{"description":"Credential authenticated but not authorized for this row (mismatched agentId / target). Body { error: \"forbidden\", message }.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorForbidden"}}}},"404":{"description":"No row matches {id}.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorNotFound"}}}},"500":{"description":"Unexpected helper failure past the contract guard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorInternal"}}}}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/api/bookings/{id}/occurrences":{"get":{"tags":["Bookings"],"operationId":"getBookingsIdOccurrences","summary":"Expand a booking RRULE into concrete slots","description":"Expands the stored RRULE into the next N concrete tstzrange slots starting from now. Default limit 50, max 200. Slots returned in chronological order (startsAt ascending). The id must belong to the authenticated agent.","parameters":[{"name":"Authorization","in":"header","required":false,"schema":{"type":"string"},"description":"Use either this Bearer <sk_…> credential or X-API-Key, not both."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Alternative credential header: X-API-Key: <sk_…> from POST /agents."},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The row id (booking, proposal, etc.) owned by the authenticated agent."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200},"description":"Cap on the number of expanded slot rows. Default 50."}],"responses":{"200":{"description":"Concrete slot items.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OccurrencesEnvelope"}}}},"400":{"description":"The stored RRULE did not parse — body { error: \"bad_rrule\", message }. The row stays; cancel + re-create.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["bad_rrule"]},"message":{"type":"string"}},"required":["error"]}}}},"401":{"description":"Missing or unparseable Authorization or X-API-Key header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorUnauthorized"}}}},"403":{"description":"Credential authenticated but not authorized for this row (mismatched agentId / target). Body { error: \"forbidden\", message }.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorForbidden"}}}},"404":{"description":"No row matches {id}.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorNotFound"}}}},"500":{"description":"Unexpected helper failure past the contract guard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorInternal"}}}}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/api/proposals":{"post":{"tags":["Proposals"],"operationId":"postProposals","summary":"Open a slot-transfer proposal","description":"Agent credential required. proposerAgentId is set server-side from the authenticated credential — NEVER trusted from the body. The proposal starts in status=\"pending\"; the target agent must accept or reject before any Booking row exists.","parameters":[{"name":"Authorization","in":"header","required":false,"schema":{"type":"string"},"description":"Use either this Bearer <sk_…> credential or X-API-Key, not both."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Alternative credential header: X-API-Key: <sk_…> from POST /agents."},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Optional client-supplied idempotency token. Agent creation rejects a reused idempotency_key with 409 conflict."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProposalCreate"}}}},"responses":{"201":{"description":"Proposal persisted in status=\"pending\".","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProposalItem"}}}},"400":{"description":"Request body or query failed Zod validation — body is { errors: { field: msg } }.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorValidation"}}}},"401":{"description":"Missing or unparseable Authorization or X-API-Key header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorUnauthorized"}}}},"500":{"description":"Unexpected helper failure past the contract guard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorInternal"}}}}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/api/proposals/{id}/accept":{"patch":{"tags":["Proposals"],"operationId":"patchProposalsIdAccept","summary":"Target agent accepts the proposal (transfers slot)","description":"PATCH transfers ownership of the resulting Booking to the proposer (Booking.agentId = proposal.proposerAgentId), so the slot flips to the proposers calendar; a booking.created webhook fires against the new owner.","parameters":[{"name":"Authorization","in":"header","required":false,"schema":{"type":"string"},"description":"Use either this Bearer <sk_…> credential or X-API-Key, not both."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Alternative credential header: X-API-Key: <sk_…> from POST /agents."},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Optional client-supplied idempotency token. Agent creation rejects a reused idempotency_key with 409 conflict."},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The row id (booking, proposal, etc.) owned by the authenticated agent."}],"responses":{"200":{"description":"Proposal accepted; booking.created fires for new owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProposalItem"}}}},"401":{"description":"Missing or unparseable Authorization or X-API-Key header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorUnauthorized"}}}},"403":{"description":"Credential authenticated but not authorized for this row (mismatched agentId / target). Body { error: \"forbidden\", message }.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorForbidden"}}}},"404":{"description":"No row matches {id}.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorNotFound"}}}},"409":{"description":"Resource conflict (slot overlap, proposal already terminal). Body { error, message }.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorConflict"}}}},"500":{"description":"Unexpected helper failure past the contract guard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorInternal"}}}}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/api/proposals/{id}/reject":{"patch":{"tags":["Proposals"],"operationId":"patchProposalsIdReject","summary":"Target agent rejects the proposal","description":"Resets status to \"pending\" so a future transfer attempt remains possible (not a terminal decline) — only the original slot owner can reject. The conditional UPDATE keeps concurrent accepts from double-flipping.","parameters":[{"name":"Authorization","in":"header","required":false,"schema":{"type":"string"},"description":"Use either this Bearer <sk_…> credential or X-API-Key, not both."},{"name":"X-API-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Alternative credential header: X-API-Key: <sk_…> from POST /agents."},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Optional client-supplied idempotency token. Agent creation rejects a reused idempotency_key with 409 conflict."},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The row id (booking, proposal, etc.) owned by the authenticated agent."}],"responses":{"200":{"description":"Proposal reset to pending.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProposalItem"}}}},"401":{"description":"Missing or unparseable Authorization or X-API-Key header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorUnauthorized"}}}},"403":{"description":"Credential authenticated but not authorized for this row (mismatched agentId / target). Body { error: \"forbidden\", message }.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorForbidden"}}}},"404":{"description":"No row matches {id}.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorNotFound"}}}},"409":{"description":"Resource conflict (slot overlap, proposal already terminal). Body { error, message }.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorConflict"}}}},"500":{"description":"Unexpected helper failure past the contract guard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorInternal"}}}}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}}}}