# 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
