900 lines
11 KiB
Markdown
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
|