> ## Documentation Index
> Fetch the complete documentation index at: https://cal.id/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Cal ID API Quickstart

> Go from a fresh Cal ID API key to a confirmed booking in five requests — authenticate, find an event type, fetch real slots, and book one of them.

This page takes you from nothing to a real booking on your own account, in five requests. Every call runs against `https://api.cal.id` and is authenticated with a bearer token. Work through it in order — each step produces a value the next one needs, and the last step **will fail** if you skip ahead and invent a time rather than using one the API gave you.

<Note>
  You need a Cal ID account with at least one event type. If you have none, create one in the dashboard first — this guide reads an existing event type rather than creating one.
</Note>

<Steps>
  <Step title="Get an API key">
    In the Cal ID dashboard, go to **Settings → Developer → API Keys** and create a key. Keys are prefixed with `calid_` and are shown **once**, so copy it somewhere safe immediately.

    See [Get your API key](/docs/developers/api-key) for the full walkthrough, including expiry options.

    <Warning>
      Your API key grants full access to your account. Never commit it to source control or put it in client-side code. Keep it in an environment variable:

      ```bash theme={null}
      export CALID_KEY="calid_xxxxx"
      ```
    </Warning>
  </Step>

  <Step title="Confirm the key works">
    Call `GET /users/me`. It is the cheapest way to prove your key and header are correct before you debug anything else.

    <CodeGroup>
      ```bash cURL theme={null}
      curl https://api.cal.id/users/me \
        -H "Authorization: Bearer $CALID_KEY"
      ```

      ```javascript JavaScript theme={null}
      async function whoAmI() {
        const res = await fetch("https://api.cal.id/users/me", {
          headers: { Authorization: `Bearer ${process.env.CALID_KEY}` },
        });

        const body = await res.json();

        if (!res.ok) {
          // 401 means the header is wrong, or the key is invalid or expired.
          throw new Error(`HTTP ${res.status}: ${body.message ?? "request failed"}`);
        }

        // GET /users/me returns data as an object.
        return body.data;
      }

      whoAmI().then((user) => console.log(user.username, user.timeZone));
      ```

      ```python Python theme={null}
      import os
      import requests

      CALID_KEY = os.environ["CALID_KEY"]

      response = requests.get(
          "https://api.cal.id/users/me",
          headers={"Authorization": f"Bearer {CALID_KEY}"},
      )

      if response.status_code == 401:
          # The header is wrong, or the key is invalid or expired.
          raise SystemExit("Unauthorized — check your API key")

      response.raise_for_status()

      # GET /users/me returns data as an object.
      user = response.json()["data"]
      print(user["username"], user["timeZone"])
      ```
    </CodeGroup>

    A working key returns your profile inside `data`:

    ```json theme={null}
    {
      "success": true,
      "data": {
        "id": 1234,
        "username": "jane-doe",
        "email": "jane@example.com",
        "timeZone": "Asia/Kolkata"
      },
      "message": "Request completed successfully"
    }
    ```

    If you get `401` with `error.code: "UNAUTHORIZED"`, the header is wrong or the key is invalid or expired. If you get a `404` whose body has **no `success` field**, you have mistyped the URL — that response comes from the router, not the API. See [Error codes](/docs/developers/error-codes).

    <Note>
      `GET /users/me` returns `data` as an object. Its sibling `GET /users/{userslug}` returns `data` as an **array** — read that one as `data[0]`.
    </Note>
  </Step>

  <Step title="Find an event type ID">
    Every booking is made against an event type. List yours and pick one.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -G https://api.cal.id/event-types/ \
        -H "Authorization: Bearer $CALID_KEY" \
        --data-urlencode "limit=100"
      ```

      ```javascript JavaScript theme={null}
      async function listEventTypes() {
        // Always pass limit explicitly: the default is 10, the maximum is 100.
        const params = new URLSearchParams({ limit: "100" });

        const res = await fetch(`https://api.cal.id/event-types/?${params}`, {
          headers: { Authorization: `Bearer ${process.env.CALID_KEY}` },
        });

        const body = await res.json();
        if (!res.ok) throw new Error(body.message ?? `HTTP ${res.status}`);

        // Records in data, paging state in meta.pagination.
        return { eventTypes: body.data, pagination: body.meta.pagination };
      }

      listEventTypes().then(({ eventTypes, pagination }) => {
        for (const e of eventTypes) console.log(e.id, e.title, e.length);
        console.log(`${eventTypes.length} of ${pagination.total}`);
      });
      ```

      ```python Python theme={null}
      import os
      import requests

      CALID_KEY = os.environ["CALID_KEY"]

      # Always pass limit explicitly: the default is 10, the maximum is 100.
      response = requests.get(
          "https://api.cal.id/event-types/",
          headers={"Authorization": f"Bearer {CALID_KEY}"},
          params={"limit": 100},
      )
      response.raise_for_status()

      body = response.json()
      for event_type in body["data"]:
          print(event_type["id"], event_type["title"], event_type["length"])

      # Paging state lives in meta.pagination.
      print(f"{len(body['data'])} of {body['meta']['pagination']['total']}")
      ```
    </CodeGroup>

    <Warning>
      `GET /event-types/` returns **10 records by default**. If you omit `limit`, an account with 40 event types silently looks like it has 10, and the one you were after may simply not be in the response. Always pass `limit` explicitly. The maximum is `100`; `?limit=1000` is rejected with `400 — querystring/limit must be <= 100`.
    </Warning>

    The list arrives in `data`, with paging state in `meta.pagination`. Note the `id` of the event type you want to book, and its `length` in minutes — you need both.

    ```json theme={null}
    {
      "success": true,
      "data": [
        { "id": 12345, "title": "30 Minute Meeting", "slug": "30min", "length": 30 }
      ],
      "meta": {
        "pagination": { "page": 1, "limit": 100, "total": 1, "totalPages": 1 }
      },
      "message": "Request completed successfully"
    }
    ```

    <Tip>
      You can also read the ID straight from the dashboard URL while editing an event — see [Find your Event Type ID](/docs/developers/event-type-id).
    </Tip>
  </Step>

  <Step title="Fetch real bookable slots">
    Ask the API which times are actually available. Do not compute this yourself: availability is the product of schedules, buffers, minimum notice, booking limits, connected-calendar busy times, and timezone rules, and a reimplementation will not match.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -G https://api.cal.id/slots/ \
        -H "Authorization: Bearer $CALID_KEY" \
        --data-urlencode "eventTypeId=12345" \
        --data-urlencode "start=2026-09-08T00:00:00.000Z" \
        --data-urlencode "end=2026-09-09T00:00:00.000Z" \
        --data-urlencode "timeZone=Asia/Kolkata"
      ```

      ```javascript JavaScript theme={null}
      async function getSlots() {
        const params = new URLSearchParams({
          // On /slots/ the event type ID is a string; on /booking/ it is an integer.
          eventTypeId: "12345",
          start: "2026-09-08T00:00:00.000Z",
          end: "2026-09-09T00:00:00.000Z",
          timeZone: "Asia/Kolkata",
        });

        const res = await fetch(`https://api.cal.id/slots/?${params}`, {
          headers: { Authorization: `Bearer ${process.env.CALID_KEY}` },
        });

        const body = await res.json();
        if (!res.ok) throw new Error(body.message ?? `HTTP ${res.status}`);

        // Slots nest as data.slots["YYYY-MM-DD"][].time — not data[0].
        return body.data.slots;
      }

      getSlots().then((slots) => {
        const day = slots["2026-09-08"] ?? [];
        if (day.length === 0) throw new Error("Nothing bookable in that window");
        console.log(day[0].time);
      });
      ```

      ```python Python theme={null}
      import os
      import requests

      CALID_KEY = os.environ["CALID_KEY"]

      response = requests.get(
          "https://api.cal.id/slots/",
          headers={"Authorization": f"Bearer {CALID_KEY}"},
          params={
              # On /slots/ the event type ID is a string; on /booking/ it is an int.
              "eventTypeId": "12345",
              "start": "2026-09-08T00:00:00.000Z",
              "end": "2026-09-09T00:00:00.000Z",
              "timeZone": "Asia/Kolkata",
          },
      )
      response.raise_for_status()

      # Slots nest as data.slots["YYYY-MM-DD"][].time — not data[0].
      slots = response.json()["data"]["slots"]
      day = slots.get("2026-09-08", [])
      if not day:
          raise SystemExit("Nothing bookable in that window")

      print(day[0]["time"])
      ```
    </CodeGroup>

    <Warning>
      On `GET /slots/`, `eventTypeId` is a **string**, not an integer. `POST /booking/` in the next step takes the same ID as an **integer**. If you build requests from a typed client or a schema, you must convert between the two — sending the wrong type is rejected with `400`.
    </Warning>

    The response nests slots two levels deep: `data.slots` is an object keyed by date (`"YYYY-MM-DD"`), and each value is an array of slot objects whose bookable time is in `time`.

    ```json theme={null}
    {
      "success": true,
      "data": {
        "slots": {
          "2026-09-08": [
            { "time": "2026-09-08T03:30:00.000Z" },
            { "time": "2026-09-08T04:00:00.000Z" }
          ]
        }
      },
      "message": "Request completed successfully"
    }
    ```

    Read your chosen time as `data.slots["2026-09-08"][0].time` — not `data[0]` and not `data.slots[0]`.

    If the date key you expected is missing, or its array is empty, there is genuinely nothing bookable in that window. Widen `start`/`end` or check the event type's schedule before moving on.
  </Step>

  <Step title="Create the booking">
    Post the slot you just read. `start`, `end`, and `responses` (with at least `name` and `email`) are required, and `end` is `start` plus the event type's `length` in minutes.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -X POST https://api.cal.id/booking/ \
        -H "Authorization: Bearer $CALID_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "eventTypeId": 12345,
          "start": "2026-09-08T03:30:00.000Z",
          "end": "2026-09-08T04:00:00.000Z",
          "timeZone": "Asia/Kolkata",
          "responses": {
            "name": "Jane Doe",
            "email": "jane@example.com"
          }
        }'
      ```

      ```javascript JavaScript theme={null}
      async function createBooking() {
        const res = await fetch("https://api.cal.id/booking/", {
          method: "POST",
          headers: {
            Authorization: `Bearer ${process.env.CALID_KEY}`,
            // Mandatory — omit it and the body is never parsed.
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            // Integer here, string on /slots/.
            eventTypeId: 12345,
            // start and end — never startTime/endTime.
            start: "2026-09-08T03:30:00.000Z",
            end: "2026-09-08T04:00:00.000Z",
            timeZone: "Asia/Kolkata",
            responses: {
              name: "Jane Doe",
              email: "jane@example.com",
            },
          }),
        });

        const body = await res.json();

        if (!res.ok) {
          if (body.message === "no_available_users_found_error") {
            // Deterministic — do not retry. Re-read GET /slots/ instead.
            throw new Error("That start time is not bookable. Re-fetch slots.");
          }
          throw new Error(`HTTP ${res.status}: ${body.message ?? "request failed"}`);
        }

        return body.data;
      }

      createBooking().then((booking) => console.log(booking.uid));
      ```

      ```python Python theme={null}
      import os
      import requests

      CALID_KEY = os.environ["CALID_KEY"]

      response = requests.post(
          "https://api.cal.id/booking/",
          headers={
              "Authorization": f"Bearer {CALID_KEY}",
              # Mandatory — omit it and the body is never parsed.
              "Content-Type": "application/json",
          },
          json={
              # Integer here, string on /slots/.
              "eventTypeId": 12345,
              # start and end — never startTime/endTime.
              "start": "2026-09-08T03:30:00.000Z",
              "end": "2026-09-08T04:00:00.000Z",
              "timeZone": "Asia/Kolkata",
              "responses": {
                  "name": "Jane Doe",
                  "email": "jane@example.com",
              },
          },
      )

      if not response.ok:
          body = response.json()
          if body.get("message") == "no_available_users_found_error":
              # Deterministic — do not retry. Re-read GET /slots/ instead.
              raise SystemExit("That start time is not bookable. Re-fetch slots.")
          response.raise_for_status()

      print(response.json()["data"]["uid"])
      ```
    </CodeGroup>

    <Warning>
      `Content-Type: application/json` is mandatory. Omit it and the body is never parsed — you get `400 — body must be object` even though the JSON is perfectly valid.
    </Warning>

    The created booking comes back in `data`, including its `uid`.

    ```json theme={null}
    {
      "success": true,
      "data": {
        "uid": "abc123XYZ",
        "start": "2026-09-08T03:30:00.000Z",
        "end": "2026-09-08T04:00:00.000Z"
      },
      "message": "Booking created successfully."
    }
    ```
  </Step>
</Steps>

## If the booking fails with a 500

The most common failure in this flow is:

```json theme={null}
{
  "success": false,
  "message": "no_available_users_found_error",
  "error": { "code": "INTERNAL_ERROR" }
}
```

Despite the `500`, this is not a Cal ID outage — it means the `start` you sent is not a time the scheduling rules will accept. Almost always the cause is that `start` did not come from Step 4.

<Warning>
  Do not retry a `no_available_users_found_error` with backoff. It is deterministic: every retry with the same `start` fails identically. Go back to `GET /slots/` and use a time it actually returned.
</Warning>

Two things that look fine but are not:

* **A time that is obviously free on your calendar.** Availability is not the same as "not busy" — buffers, minimum notice, and booking limits all remove otherwise-empty times.
* **A time echoed back from a booking object.** Booking *responses* use `startTime` and `endTime`; the request body uses `start` and `end`. You cannot round-trip a booking object straight back into `POST /booking/`.

## What you just learned

| Behaviour                                                         | Why it matters                                      |
| ----------------------------------------------------------------- | --------------------------------------------------- |
| `GET /event-types/` defaults to `limit=10`                        | Missing records look like deleted records.          |
| `eventTypeId` is a string on `/slots/`, an integer on `/booking/` | Typed clients need an explicit conversion.          |
| Slots nest as `data.slots["YYYY-MM-DD"][].time`                   | `data[0]` returns `undefined`, not an error.        |
| `Content-Type: application/json` is mandatory on `POST`           | Omitting it produces a `400` that blames your body. |
| `start` must be a time `/slots/` returned                         | Otherwise you get a `500` that no retry will fix.   |

## Go deeper with the API

<CardGroup cols={2}>
  <Card title="API integration" icon="plug" href="/docs/developers/api-integration">
    The conventions that apply across every endpoint — envelopes, pagination, and the traps.
  </Card>

  <Card title="Error codes" icon="triangle-exclamation" href="/docs/developers/error-codes">
    Every `error.code` the API returns, and what to do about each.
  </Card>

  <Card title="Reserve a slot before booking" icon="lock" href="/docs/developers/guides/reserve-a-slot">
    Hold a time while the booker completes checkout, so nobody else takes it.
  </Card>

  <Card title="Reschedule and cancel" icon="calendar-xmark" href="/docs/developers/guides/reschedule-and-cancel">
    Move or cancel a booking you created.
  </Card>

  <Card title="Webhooks" icon="webhook" href="/docs/developers/webhooks">
    Get booking events pushed to you instead of polling.
  </Card>

  <Card title="API Reference" icon="code" href="/docs/api-reference/introduction">
    Every endpoint, with schemas and a live "Try it" playground.
  </Card>
</CardGroup>


## Related topics

- [How to Get Your Cal ID API Key](/docs/developers/api-key.md)
- [Cal ID API Integration](/docs/developers/api-integration.md)
- [Cal ID API Error Codes](/docs/developers/error-codes.md)
- [How to Find Your Event Type ID](/docs/developers/event-type-id.md)
- [Cal ID Scheduling API Reference](/docs/api-reference/introduction.md)
