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

900 lines
11 KiB
Markdown

# 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 <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](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/)<token>",
"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