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

# Push calendar events

> Already sync your users’ calendars? Forward each event as you receive it, and MeetingKit turns it into a meeting.

If your product already syncs calendars through Google, Microsoft Graph or Nylas, you
don't need your users to connect theirs a second time. Forward each event you receive
to MeetingKit, as it is. Every push creates or updates one
[meeting](/api-reference/meetings/object) with `source: "calendar_sync"`.

Push events or [connect calendars](/meetingkit/calendar-connection), not both, in one
workspace: see [How meetings get in](/meetingkit/getting-meetings-in).

## Push an event

Send the event exactly as your provider gave it to you, with the member it belongs to:

```bash theme={null}
curl -X POST "https://api.meetingkit.com/api/v1/calendar_events" \
  -H "Authorization: $MEETINGKIT_API_KEY" \
  -H "Happyscribe-Version: 2026-10-12" \
  -H "Content-Type: application/json" \
  -d '{
    "workspace_id": "'"$WORKSPACE_ID"'",
    "member_email": "laia@gurusup.com",
    "event": {
      "kind": "calendar#event",
      "id": "7kq2bqr1t0c9",
      "iCalUID": "7kq2bqr1t0c9@google.com",
      "status": "confirmed",
      "summary": "Weekly sync with Acme",
      "start": { "dateTime": "2026-09-29T09:00:00Z" },
      "end": { "dateTime": "2026-09-29T09:30:00Z" },
      "hangoutLink": "https://meet.google.com/abc-defg-hij",
      "organizer": { "email": "laia@gurusup.com" },
      "attendees": [
        { "email": "laia@gurusup.com", "responseStatus": "accepted" },
        { "email": "ana@acme.com", "displayName": "Ana Ruiz", "responseStatus": "needsAction" }
      ]
    }
  }'
```

The response is `200` with the [calendar event](/api-reference/calendar-events/object):
the occurrence as MeetingKit sees it, with its `cev_` id and the `meeting_id` of the
meeting it belongs to: one per meeting, shared by every member who has the event. Add a
top-level `metadata` to keep your own ids on it; it is merged on every push. Whether a bot
joins follows the member's [recording choices](/meetingkit/meeting-settings#record-my-meetings).

* Pass `member_email` or `member_id`, not both. An email that is not a member yet
  creates one, invisibly. Members created this way count toward
  the limit of 100 new members an hour per workspace.
* The request is at most 64 KB. The event's description is used only to find the meeting link and
  is never stored.

### Accepted events

| Provider | What to send |
| - | - |
| Google Calendar | The [`events` resource](https://developers.google.com/calendar/api/v3/reference/events), including `kind`. |
| Microsoft Graph | The [`event` resource](https://learn.microsoft.com/graph/api/resources/event). Request it with `Prefer: outlook.timezone="UTC"`: Windows time zone names are rejected. |
| Nylas v3 | The event, bare or as the `data.object` of a Nylas webhook. |

Without a provider, send the minimal shape instead:

```json theme={null}
{
  "ical_uid": "b3f1a9c2@gurusup.com",
  "original_starts_at": null,
  "title": "Weekly sync with Acme",
  "starts_at": "2026-09-29T09:00:00Z",
  "ends_at": "2026-09-29T09:30:00Z",
  "meeting_url": "https://meet.google.com/abc-defg-hij",
  "organizer": { "email": "laia@gurusup.com", "name": "Laia Puig" },
  "participants": [{ "email": "ana@acme.com", "name": "Ana Ruiz", "status": "pending" }],
  "cancelled": false
}
```

`ical_uid` is required: if your events have none, send any string that is stable and
unique per event. Times without an offset are read as UTC. An event with no end lasts
30 minutes; an all-day Nylas event lasts the day.

## When events change

Push the event again every time your provider tells you it changed. Each push replaces
the member's copy of the event.

* **Rescheduled or new link:** the meeting keeps its `mtg_` id, and its bot moves.
* **Colleagues on the same call:** push each member's copy. Copies with the same iCal
  UID land in one meeting, with one bot.
* **Retries and backfills:** pushing the same event again is safe.
* **Past events:** accepted. They become `past` meetings with their participants, and no
  bot is sent.
* **No meeting link:** the meeting exists, with recording off
  (`settings.sources["recording.enabled"].reason: "no_meeting_url"`) until a push brings a link. Only Google Meet,
  Zoom and Microsoft Teams links count: a Webex or other link reads as no link.

### Recurring events

Push each occurrence separately. Series masters aren't supported yet: one is refused with
`422 recurring_series_not_supported`. Providers expand occurrences for you (Google
`singleEvents=true`, Graph `calendarView`, Nylas `expand_recurring`).

<Warning>
  An occurrence that was moved must keep its original start: `originalStartTime` in Google,
  `originalStart` in Graph, `original_start_time` in Nylas, or `original_starts_at` in the minimal
  shape. Without it, the moved occurrence becomes a second meeting.
</Warning>

### Cancelled events

Push the cancelled event: Google `status: "cancelled"` (the bare
`{ "id", "status": "cancelled" }` Google sends for a deleted event works), Graph
`isCancelled: true` or a delta `@removed`, Nylas `status: "cancelled"` or an
`event.deleted` webhook, or `cancelled: true` in the minimal shape.

The member's copy is removed. When no member of the workspace still has the event,
the meeting moves to `phase: "cancelled"`, the calendar event gets its `cancelled_at`, and its bot
is cancelled. Pushing the event
again, live, brings it back.

## Errors and limits

| Status | `code` | Meaning |
| - | - | - |
| `404` | `not_found` | Unknown member, or a cancelled event that was never pushed. |
| `409` | `busy` | Another push for the same occurrence is being applied. Retry after `Retry-After`. |
| `413` | `too_large` | The request is over 64 KB. |
| `422` | `unrecognised_event` | Not a Google, Graph or Nylas event, nor the minimal shape. |
| `422` | `recurring_series_not_supported` | A series master. Push occurrences. |
| `422` | `missing`, `invalid` | A missing `ical_uid` or start, an end before the start, an unreadable time zone. `field` says which. |
| `429` | `rate_limited` | More than 1,000 pushes a minute in the workspace, or a push that would create a member past 100 an hour. Retry after `Retry-After`. |
