first commit
Made-with: Cursor
This commit is contained in:
899
about/details.md
Normal file
899
about/details.md
Normal file
@@ -0,0 +1,899 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user