# Calendar & Contacts API ## details.md This document defines exact endpoint contracts: request/response schemas, field constraints, pagination formats, examples, and implementation notes so a developer can build the API and a frontend without guessing. All timestamps are RFC3339 strings. All stored times are UTC. --- # 1. Conventions ## 1.1 Base URL * Local: [http://localhost:8080](http://localhost:8080) * Production: [https://api.example.com](https://api.example.com) All endpoints are under the root path. --- ## 1.2 Authentication Headers JWT: * Authorization: Bearer API key: * X-API-Key: If both are present, JWT takes precedence. --- ## 1.3 Standard Response Envelope This API returns plain JSON objects. For list endpoints, a consistent list envelope is required. List response envelope: { "items": [ ... ], "page": { "limit": 50, "next_cursor": "opaque-or-null" } } --- ## 1.4 Cursor Pagination Query params: * limit (optional, default 50, max 200) * cursor (optional) Cursor meaning: * Opaque base64url string encoding the tuple: * last_sort_time (RFC3339) * last_id (uuid) Sorting rule for paginated lists: * Primary: start_time asc (or created_at for contacts) * Secondary: id asc If cursor is provided: * Return records strictly greater than the tuple. --- ## 1.5 Error Format All error responses: { "error": "Human readable message", "code": "MACHINE_READABLE_CODE", "details": "Optional string or object" } HTTP mapping: * 400 VALIDATION_ERROR * 401 AUTH_REQUIRED / AUTH_INVALID * 403 FORBIDDEN * 404 NOT_FOUND * 409 CONFLICT * 429 RATE_LIMITED * 500 INTERNAL --- # 2. Data Schemas ## 2.1 User { "id": "uuid", "email": "string", "timezone": "string", "created_at": "RFC3339", "updated_at": "RFC3339" } Constraints: * email lowercase * timezone must be IANA timezone name, default "UTC" --- ## 2.2 Calendar { "id": "uuid", "name": "string", "color": "string", "is_public": true, "role": "owner|editor|viewer", "created_at": "RFC3339", "updated_at": "RFC3339" } Constraints: * name 1..80 * color is hex like "#RRGGBB" (optional) --- ## 2.3 Event { "id": "uuid", "calendar_id": "uuid", "title": "string", "description": "string|null", "location": "string|null", "start_time": "RFC3339-UTC", "end_time": "RFC3339-UTC", "timezone": "string", "all_day": false, "recurrence_rule": "string|null", "created_by": "uuid", "updated_by": "uuid", "created_at": "RFC3339", "updated_at": "RFC3339", "reminders": [ {"id": "uuid", "minutes_before": 10} ], "attendees": [ {"id": "uuid", "user_id": "uuid|null", "email": "string|null", "status": "pending|accepted|declined|tentative"} ], "tags": ["string"], "attachments": [ {"id": "uuid", "file_url": "string"} ] } Constraints: * title 1..140 * timezone IANA name * recurrence_rule must be valid RFC5545 RRULE when present --- ## 2.4 Contact { "id": "uuid", "first_name": "string|null", "last_name": "string|null", "email": "string|null", "phone": "string|null", "company": "string|null", "notes": "string|null", "created_at": "RFC3339", "updated_at": "RFC3339" } Constraints: * At least one of: first_name, last_name, email, phone must be present --- # 3. Endpoint Contracts ## 3.1 Auth ### POST /auth/register Request: { "email": "[user@example.com](mailto:user@example.com)", "password": "string", "timezone": "America/Asuncion" } Rules: * timezone optional * server creates default calendar Response 200: { "user": { ...User }, "access_token": "string", "refresh_token": "string" } Errors: * 400 VALIDATION_ERROR * 409 CONFLICT (email already exists) --- ### POST /auth/login Request: { "email": "[user@example.com](mailto:user@example.com)", "password": "string" } Response 200: { "user": { ...User }, "access_token": "string", "refresh_token": "string" } Errors: * 401 AUTH_INVALID --- ### POST /auth/refresh Request: { "refresh_token": "string" } Response 200: { "access_token": "string", "refresh_token": "string" } Errors: * 401 AUTH_INVALID --- ### POST /auth/logout Request: { "refresh_token": "string" } Response 200: { "ok": true } --- ### GET /auth/me Response 200: { "user": { ...User } } --- ## 3.2 API Keys ### POST /api-keys Request: { "name": "My agent key", "scopes": { "calendars": ["read", "write"], "events": ["read", "write"], "contacts": ["read", "write"], "availability": ["read"], "booking": ["write"] } } Response 200: { "id": "uuid", "name": "My agent key", "created_at": "RFC3339", "token": "RAW_TOKEN_RETURNED_ONCE" } --- ### GET /api-keys Response 200: { "items": [ {"id": "uuid", "name": "string", "created_at": "RFC3339", "revoked_at": "RFC3339|null"} ], "page": {"limit": 50, "next_cursor": null} } --- ### DELETE /api-keys/{id} Response 200: { "ok": true } --- ## 3.3 Users ### GET /users/me Response 200: { "user": { ...User } } --- ### PUT /users/me Request: { "timezone": "America/Asuncion" } Response 200: { "user": { ...User } } --- ### DELETE /users/me Response 200: { "ok": true } --- ## 3.4 Calendars ### GET /calendars Response 200: { "items": [ ...Calendar ], "page": {"limit": 50, "next_cursor": null} } --- ### POST /calendars Request: { "name": "Work", "color": "#22C55E" } Response 200: { "calendar": { ...Calendar } } --- ### GET /calendars/{id} Response 200: { "calendar": { ...Calendar } } --- ### PUT /calendars/{id} Request: { "name": "Work Calendar", "color": "#22C55E", "is_public": false } Rules: * is_public only owner Response 200: { "calendar": { ...Calendar } } --- ### DELETE /calendars/{id} Response 200: { "ok": true } --- ## 3.5 Calendar Sharing ### POST /calendars/{id}/share Request: { "target": {"email": "[other@example.com](mailto:other@example.com)"}, "role": "editor" } Response 200: { "ok": true } --- ### GET /calendars/{id}/members Response 200: { "items": [ {"user_id": "uuid", "email": "string", "role": "owner|editor|viewer"} ], "page": {"limit": 50, "next_cursor": null} } --- ### DELETE /calendars/{id}/members/{user_id} Response 200: { "ok": true } --- ## 3.6 Events ### GET /events Query: * start (required) RFC3339 * end (required) RFC3339 * calendar_id (optional) * search (optional) * tag (optional) * limit, cursor Response 200: { "items": [ ...Event ], "page": {"limit": 50, "next_cursor": "string|null"} } Notes: * Must include expanded recurrence occurrences inside requested range. * For recurrence expansion, include occurrences as separate items with: * id = master event id * occurrence_start_time, occurrence_end_time (recommended fields) Recommended recurrence occurrence representation: { "id": "uuid-master", "is_occurrence": true, "occurrence_start_time": "RFC3339", "occurrence_end_time": "RFC3339", ...base event fields... } --- ### POST /events Request: { "calendar_id": "uuid", "title": "Meeting", "description": "Project sync", "location": "Zoom", "start_time": "2026-03-01T14:00:00-03:00", "end_time": "2026-03-01T15:00:00-03:00", "timezone": "America/Asuncion", "all_day": false, "recurrence_rule": null, "reminders": [10, 60], "tags": ["work", "sync"] } Response 200: { "event": { ...Event } } Errors: * 403 FORBIDDEN * 400 VALIDATION_ERROR --- ### GET /events/{id} Response 200: { "event": { ...Event } } --- ### PUT /events/{id} Request: { "title": "Updated title", "start_time": "2026-03-01T15:00:00-03:00", "end_time": "2026-03-01T16:00:00-03:00", "recurrence_rule": "FREQ=WEEKLY;BYDAY=MO,WE,FR" } Response 200: { "event": { ...Event } } --- ### DELETE /events/{id} Response 200: { "ok": true } --- ## 3.7 Event Reminders ### POST /events/{id}/reminders Request: { "minutes_before": [5, 15, 60] } Response 200: { "event": { ...Event } } --- ### DELETE /events/{id}/reminders/{reminder_id} Response 200: { "ok": true } --- ## 3.8 Attendees ### POST /events/{id}/attendees Request: { "attendees": [ {"email": "[guest@example.com](mailto:guest@example.com)"}, {"user_id": "uuid"} ] } Response 200: { "event": { ...Event } } --- ### PUT /events/{id}/attendees/{attendee_id} Request: { "status": "accepted" } Rules: * Organizer can edit any attendee * Attendee can edit own status Response 200: { "event": { ...Event } } --- ### DELETE /events/{id}/attendees/{attendee_id} Response 200: { "ok": true } --- ## 3.9 Contacts ### GET /contacts Query: * search (optional) * limit, cursor Response 200: { "items": [ ...Contact ], "page": {"limit": 50, "next_cursor": "string|null"} } --- ### POST /contacts Request: { "first_name": "Jane", "last_name": "Doe", "email": "[jane@example.com](mailto:jane@example.com)", "phone": "+595981000000", "company": "Example SA", "notes": "Met at event" } Response 200: { "contact": { ...Contact } } --- ### GET /contacts/{id} Response 200: { "contact": { ...Contact } } --- ### PUT /contacts/{id} Request: { "notes": "Updated notes" } Response 200: { "contact": { ...Contact } } --- ### DELETE /contacts/{id} Response 200: { "ok": true } --- ## 3.10 Availability ### GET /availability Query: * calendar_id (required) * start (required) * end (required) Response 200: { "calendar_id": "uuid", "range_start": "RFC3339", "range_end": "RFC3339", "busy": [ {"start": "RFC3339", "end": "RFC3339", "event_id": "uuid"} ] } --- ## 3.11 Booking Links ### POST /calendars/{id}/booking-link Request: { "duration_minutes": 30, "buffer_minutes": 0, "timezone": "America/Asuncion", "working_hours": { "mon": [{"start": "09:00", "end": "17:00"}], "tue": [{"start": "09:00", "end": "17:00"}], "wed": [{"start": "09:00", "end": "17:00"}], "thu": [{"start": "09:00", "end": "17:00"}], "fri": [{"start": "09:00", "end": "17:00"}], "sat": [], "sun": [] }, "active": true } Response 200: { "token": "string", "public_url": "[https://app.example.com/booking/](https://app.example.com/booking/)", "settings": { ...same-as-request } } --- ### GET /booking/{token}/availability Query: * start * end Response 200: { "token": "string", "timezone": "America/Asuncion", "duration_minutes": 30, "slots": [ {"start": "RFC3339", "end": "RFC3339"} ] } --- ### POST /booking/{token}/reserve Request: { "name": "Visitor Name", "email": "[visitor@example.com](mailto:visitor@example.com)", "slot_start": "RFC3339", "slot_end": "RFC3339", "notes": "Optional" } Response 200: { "ok": true, "event": { ...Event } } Errors: * 409 CONFLICT if slot no longer available --- ## 3.12 ICS ### GET /calendars/{id}/export.ics Response: * Content-Type: text/calendar * Body: ICS format --- ### POST /calendars/import Request: * multipart/form-data * calendar_id (uuid) * file (.ics) Response 200: { "ok": true, "imported": { "events": 12 } } --- # 4. Validation Constraints Summary Users: * email unique * password >=10 chars Calendars: * name 1..80 * color valid hex Events: * title 1..140 * end_time > start_time * timezone valid IANA * reminders minutes_before must be 0..10080 Contacts: * at least one identifying field --- # 5. Implementation Notes (Go) ## Handler Layer * Parse JSON * Validate basic constraints * Pass to service ## Service Layer * Permission enforcement * Ownership validation * Time conversion to UTC * Recurrence validation and expansion * Reminder job scheduling * Transaction management for booking reservations ## Repository Layer * sqlc queries * No business logic --- # 6. Frontend and Agent Integration Guarantees * The API must remain consistent in response shape. * List endpoints always return items + page. * All objects include created_at/updated_at. * Calendar list includes role. * Event list returns occurrences for recurrence within range. * Booking endpoints require no auth. --- End of details.md