Files
CalendarApi/about/details.md
Michilis 41f6ae916f first commit
Made-with: Cursor
2026-02-28 02:17:55 +00:00

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

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"} ] }


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