Skip to main content

API reference

Sync Jobby.dev events to Google Calendar

Use Google Calendar API + Jobby.dev event webhooks to mirror your live-event schedule into the team calendar.

Last updated

Mirror your scheduled Jobby.dev live events into a Google Calendar so the rest of your team knows when you're in interview mode. ~30 minutes including OAuth setup.

What we're building

A small server that subscribes to Jobby.dev event lifecycle webhooks and creates / updates / deletes Google Calendar entries to match. One-way sync (Jobby.dev → Calendar); calendar edits don't propagate back.

1. Set up Google OAuth

At Google Cloud Console:

  • Create a project (or use an existing one).
  • Enable the Calendar API.
  • Create OAuth credentials with the https://www.googleapis.com/auth/calendar.events scope.
  • Run the OAuth flow once locally to mint a refresh token. Store the refresh token + client ID + client secret on your server.

2. Subscribe to event webhooks

We need three events to mirror correctly: create, go-live, end.

curl https://jobby.dev/api/v1/webhooks \
  -H "Authorization: Bearer $JOBBYDEV_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "target_url": "https://my-server.example.com/calendar-sync",
    "events": [
      "event.created",
      "event.went_live",
      "event.ended",
      "event.scheduled_window_changed"
    ]
  }'

Note: event.*webhooks aren't shipped in the initial v1 cut — they're on the Tier 6 webhook expansion backlog. For now you'll need to poll jobbydev_event_list on a 5-minute cron and diff against your last-known state. Same end result, slightly more complex code.

3. Endpoint stub

import express from "express";
import { google } from "googleapis";

const app = express();
const calendar = google.calendar({ version: "v3", auth: getOAuthClient() });
const CALENDAR_ID = process.env.TEAM_CALENDAR_ID; // e.g. team@yourcompany.com

app.post(
  "/calendar-sync",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    if (!verifyJobbydevSignature(req)) return res.status(400).send();
    const event = JSON.parse(req.body.toString("utf8"));

    switch (event.type) {
      case "event.created":
      case "event.scheduled_window_changed":
        await upsertCalendarEvent(event.data);
        break;
      case "event.ended":
        await deleteCalendarEvent(event.data.event_id);
        break;
    }
    res.status(200).send("ok");
  },
);

4. Upsert helper

async function upsertCalendarEvent(data) {
  const summary = `Jobby.dev: ${data.title}`;
  const description = `Live event on Jobby.dev — ${data.role_count} role(s) attached.\nhttps://jobby.dev/events/${data.slug}`;

  // Use Jobby.dev's event_id as the Google Calendar extended property
  // for a stable foreign key
  const existing = await findExistingEvent(data.event_id);

  const eventBody = {
    summary,
    description,
    start: { dateTime: data.starts_at, timeZone: "UTC" },
    end: { dateTime: data.ends_at, timeZone: "UTC" },
    extendedProperties: {
      private: { jobbydev_event_id: data.event_id },
    },
  };

  if (existing) {
    await calendar.events.update({
      calendarId: CALENDAR_ID,
      eventId: existing.id,
      requestBody: eventBody,
    });
  } else {
    await calendar.events.insert({
      calendarId: CALENDAR_ID,
      requestBody: eventBody,
    });
  }
}

5. Idempotency

Critical here. The webhook subsystem retries on timeout, so you could process the same event.created twice. Using extendedProperties to find the existing entry by jobbydev_event_id (rather than naively inserting) makes the upsert idempotent — the second delivery sees the entry and updates instead of duplicating.

6. Test

Schedule a Jobby.dev event with jobbydev_create_event and confirm a Calendar entry appears within ~5 seconds. Update the window; confirm the Calendar entry updates. End the event; confirm the Calendar entry is removed.

Limitations

  • One-way only. Calendar edits don't propagate to Jobby.dev.
  • Time zones: we send UTC. Google Calendar respects the per-user timezone for display.
  • Attendees: not mirrored. The Calendar entry is informational — actual recruiter access to the live event is gated through Jobby.dev.

Related reading