hour
Developer

API Reference

Version 2026-05-16 — Base URL: https://hour.app

The Hour API lets you read availability, request bookings, and manage calendar events. All responses are JSON. Generate your API key and secret from the API settings page.

1. Authentication

Authenticated endpoints require an API key (hour_pk_…) and API secret (hour_sk_…). Pass them as custom headers or as HTTP Basic auth.

# Custom headers
curl https://hour.app/api/v1/me/calendars \
  -H "x-hour-api-key: hour_pk_…" \
  -H "x-hour-api-secret: hour_sk_…"

# HTTP Basic auth (api_key:api_secret, Base64-encoded)
curl https://hour.app/api/v1/me/calendars \
  -u "hour_pk_…:hour_sk_…"

Public scheduling endpoints do not require authentication.

2. GET /api/v1/profiles/:profile

Returns public profile info and links to the availability and booking endpoints. No authentication required.

curl https://hour.app/api/v1/profiles/lsgrep
{
  "profile": {
    "username": "lsgrep",
    "display_name": "Yusup",
    "profile_url": "https://hour.app/lsgrep"
  },
  "actions": {
    "read_availability": { "method": "GET", "url": "…" },
    "request_booking":   { "method": "POST", "url": "…" }
  }
}

3. GET /api/v1/profiles/:profile/availability

Returns open booking slots. Each slot includes a slot_id for use with the booking endpoint.

Query params: from (ISO 8601, default now), to (ISO 8601, max 120 days), limit (default 80, max 200).

curl "https://hour.app/api/v1/profiles/lsgrep/availability?limit=5"
{
  "slots": [
    {
      "id": "…",
      "start": "2026-05-20T09:00:00Z",
      "end":   "2026-05-20T09:30:00Z",
      "duration_minutes": 30,
      "event_type": { "title": "30-minute meeting" }
    }
  ]
}

4. POST /api/v1/profiles/:profile/bookings

Creates a booking request for a slot. The request is pending until the host accepts or rejects it.

Required body fields: slot_id, booker_email. Optional: notes (max 2000 chars).

curl -X POST https://hour.app/api/v1/profiles/lsgrep/bookings \
  -H "Content-Type: application/json" \
  -d '{"slot_id":"…","booker_email":"alice@example.com"}'
{
  "booking": {
    "id": "…",
    "status": "requested",
    "start": "2026-05-20T09:00:00Z",
    "end":   "2026-05-20T09:30:00Z"
  }
}

5. GET /api/v1/me/calendars

Returns all connected calendar accounts. Requires authentication.

curl https://hour.app/api/v1/me/calendars \
  -H "x-hour-api-key: hour_pk_…" \
  -H "x-hour-api-secret: hour_sk_…"
{
  "calendars": [
    { "id": "…", "provider": "google", "email": "you@gmail.com" }
  ]
}

6. GET /api/v1/me/events

Lists calendar events across all connected accounts. Requires authentication.

Query params: start, end (ISO 8601; default last 7 days to next 14 days), calendar_account_id to filter to one account.

curl "https://hour.app/api/v1/me/events?start=2026-05-01T00:00:00Z" \
  -H "x-hour-api-key: hour_pk_…" \
  -H "x-hour-api-secret: hour_sk_…"
{
  "events": [
    {
      "id": "…",
      "title": "Team sync",
      "start": "2026-05-20T10:00:00Z",
      "end":   "2026-05-20T11:00:00Z"
    }
  ],
  "errors": []
}

7. POST /api/v1/me/events

Creates a calendar event. Requires authentication. Required fields: title, start, end (ISO 8601). Optional: description, attendees (array of { email }), conference (adds Google Meet, default true), calendar_account_id, time_zone.

curl -X POST https://hour.app/api/v1/me/events \
  -H "x-hour-api-key: hour_pk_…" \
  -H "x-hour-api-secret: hour_sk_…" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Strategy call",
    "start": "2026-05-21T14:00:00Z",
    "end":   "2026-05-21T15:00:00Z",
    "attendees": [{"email":"bob@example.com"}]
  }'

8. Rate limits

Public endpoints are limited to 30 requests per minute per IP. Authenticated endpoints are limited to 60 requests per minute per API key. When exceeded the API returns 429 Too Many Requests with a Retry-After header.

9. Errors

All error responses include an error string and an appropriate HTTP status code: 400 bad request, 401 unauthorized, 404 not found, 409 slot unavailable, 429 rate limited, 500 server error.

{ "error": "Slot is no longer available", "code": "slot_unavailable" }