# Write a provider (https://www.cal-sdk.com/docs/build-a-provider)

A provider is a function that returns a plain object implementing `CalendarProviderV1` from `@cal-sdk/provider`. Its job is narrow on purpose: translate calsdk's requests into one API's requests, and the responses back into calsdk's shapes. What would be the same for every calendar lives in `@cal-sdk/core` instead (validation, retries, pagination loops, the free/busy fallback and warning logging), so you only write the part that's specific to your API.

```npm
npm install @cal-sdk/provider @cal-sdk/provider-utils zod
npm install -D @cal-sdk/provider-tests
```

The official providers are built on exactly these packages, so yours gets the same tools.

## The contract [#the-contract]

Here's an outline of the type. The package has the full option and result types for each method.

```ts
type CalendarProviderV1<Provider extends string, Types extends ProviderTypesV1> = {
  readonly specificationVersion: 'v1';
  readonly provider: Provider; // also the key in providerOptions and providerMetadata
  readonly capabilities: CalendarCapabilitiesV1;

  doListCalendars(options): PromiseLike<{ calendars; warnings }>;
  doGetEvent(options): PromiseLike<{ event; warnings }>;
  doListEvents(options): PromiseLike<{ events; nextPageToken?; warnings }>;
  doCreateEvent(options): PromiseLike<{ event; warnings }>;
  doUpdateEvent(options): PromiseLike<{ event; warnings }>;
  doDeleteEvent(options): PromiseLike<{ warnings }>;

  doFreeBusy?(options): PromiseLike<{ calendars; warnings }>; // calsdk falls back to doListEvents
  doSyncEvents?(options): PromiseLike<{ changes; nextPageToken?; nextCursor?; warnings }>;
};
```

The types catch shape mistakes. These rules cover what they can't:

- **Declare capabilities honestly.** calsdk picks fallbacks from them, and the conformance suite only tests what you declare as `true`.
- **Resolve `'primary'`** to the user's default calendar.
- **Keep progress in cursors.** Calls may run at once from many places, so never keep sync position or other per-call state in the provider. Caching setup that doesn't change, such as CalDAV's server discovery, is fine.
- **Return exact times.** Timed `dateTime`s carry an offset, and `timeZone` is an IANA name, so every reader gets the same instant.
- **Map errors** to calsdk's classes, so apps can catch `NotFoundError` or `ConflictError` the same way for every calendar. `mapAPICallError` does this for HTTP status codes.
- **Never save something else.** If the API can't store what was asked, throw `UnsupportedFunctionalityError`: a meeting on the wrong day is worse than an error. If you drop a harmless part, such as a color, return an `unsupported` warning instead.
- **Pass `abortSignal` and `headers`** to every request, so callers can cancel a call and add their own headers.
- **Read only your own key** in `providerOptions`. Apps may pass options for several providers at once.

## A minimal provider [#a-minimal-provider]

```ts title="acme.ts"
import {
  type CalendarProviderV1,
  type EventV1,
  UnsupportedFunctionalityError,
} from '@cal-sdk/provider';
import {
  type AccessToken,
  createJsonFetcher,
  type FetchFunction,
  mapAPICallError,
  urlPath,
} from '@cal-sdk/provider-utils';
import { z } from 'zod';

const eventSchema = z.object({
  id: z.string(),
  name: z.string(),
  starts: z.string(),
  ends: z.string(),
  zone: z.string(),
});

function fromAcme(raw: z.infer<typeof eventSchema>, calendarId: string): EventV1 {
  return {
    id: raw.id,
    calendarId,
    kind: 'single',
    title: raw.name,
    start: { dateTime: raw.starts, timeZone: raw.zone },
    end: { dateTime: raw.ends, timeZone: raw.zone },
    status: 'confirmed',
    showAs: 'busy',
  };
}

type AcmeSettings = { accessToken: AccessToken; fetch?: FetchFunction };

export function acme(settings: AcmeSettings): CalendarProviderV1<'acme'> {
  const request = createJsonFetcher({ baseURL: 'https://api.acme.example/v1', ...settings });

  return {
    specificationVersion: 'v1',
    provider: 'acme',
    capabilities: {
      events: {
        details: true,
        allDay: false,
        recurrence: false,
        recurrenceScopeThisAndFollowing: false,
        serverSideExpansion: false,
        attendees: false,
        conference: false,
        reminders: false,
        color: false,
        etag: false,
        search: false,
      },
      freeBusy: false,
      sync: false,
      webhooks: false,
    },

    async doGetEvent({ calendarId, eventId, abortSignal, headers }) {
      try {
        const { value } = await request({
          method: 'GET',
          path: urlPath`/calendars/${calendarId}/events/${eventId}`,
          schema: eventSchema,
          abortSignal,
          headers,
        });
        return { event: fromAcme(value, calendarId), warnings: [] };
      } catch (error) {
        throw mapAPICallError(error, { provider: 'acme', resource: { type: 'event', id: eventId } });
      }
    },

    async doCreateEvent({ calendarId, event }) {
      if (event.recurrence) {
        throw new UnsupportedFunctionalityError({ provider: 'acme', functionality: 'recurring events' });
      }
      // ...
    },

    // doListCalendars, doListEvents, doUpdateEvent, doDeleteEvent
  };
}
```

`createJsonFetcher` sends the bearer token, resolving it per request, and validates responses with any Standard Schema library such as zod. It never follows redirects and never puts request headers in its errors, so the token can't leak to another host or into your logs. `urlPath` encodes ids, which can contain `@`, `/` and `#`.

## Type your options and metadata [#type-your-options-and-metadata]

Declare what each operation reads from `providerOptions`, and what you put in `providerMetadata`, and calsdk type-checks them against the provider passed as `calendar`:

```ts
type AcmeTypes = {
  options: { createEvent: { notifyRoom?: boolean } };
  eventMetadata: { roomId?: string };
};

export function acme(settings: AcmeSettings): CalendarProviderV1<'acme', AcmeTypes> {
  // ...
}
```

Validate the options at runtime too, since callers may not use TypeScript. `parseProviderOptions` reads your key, throws `InvalidArgumentError` for wrong values and warns about unknown keys:

```ts
const { options, warnings } = await parseProviderOptions({
  provider: 'acme',
  providerOptions,
  schema: z.object({ notifyRoom: z.boolean().optional() }),
});
```

## Next [#next]

<Cards>
  <Card title="Test it" href="/docs/build-a-provider/conformance">
    Run the conformance suite the official providers pass.
  </Card>
  <Card title="Helpers" href="/docs/build-a-provider/provider-utils">
    Fetching, errors, options, time zones and recurrence.
  </Card>
</Cards>

When you're unsure what a method should return, read the [mock provider](https://github.com/vkpdeveloper/calsdk/tree/main/packages/mock). It implements every operation in memory, with no API in the way, and is the clearest reference.
