11 KiB
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
- Production: https://api.example.com
All endpoints are under the root path.
1.2 Authentication Headers
JWT:
- Authorization: Bearer <access_token>
API key:
- X-API-Key: <api_key_token>
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", "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", "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"}, "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"}, {"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", "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/", "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", "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