Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Calendly Laravel Package

jeffersongoncalves/laravel-calendly

Integrate Calendly scheduling into Laravel with this lightweight package. Manage scheduling links, events, invitees, and webhooks through a clean, configurable API wrapper, making it easy to sync bookings and automate scheduling workflows in your PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a thin REST client for Calendly's v2 API — it does not model Calendly resources as Eloquent models, and it does not give you a booking engine. Treat it as a typed-ish HTTP wrapper and expect to write your own service layer on top.

Minimum setup:

composer require <vendor>/laravel-calendly
php artisan vendor:publish --tag=calendly-config

Then set the token in .env:

CALENDLY_TOKEN=eyJ...

Generate a Personal Access Token from Calendly → Integrations → API & Webhooks. The token is scoped to a single Calendly user (the one who created it), not to an organization.

First call to make — always — is users/me:

$me = Calendly::users()->me();

$userUri = $me['resource']['uri'];                       // https://api.calendly.com/users/XXXX
$orgUri  = $me['resource']['current_organization'];     // https://api.calendly.com/organizations/YYYY

Cache both of those (they never change for a given token). Almost every other endpoint needs one of them as a required filter — event_types, scheduled_events, organization_memberships, and webhook subscription creation all take a user or organization URI, not an ID. If you skip this step you will spend an hour passing UUIDs and getting 400s.

First real use case: list event types for the token owner and read the scheduling_url off each one to embed a booking button.

Implementation Patterns

Wrap it, don't sprinkle it. The client returns raw arrays. Put a single CalendlyService behind the container that resolves and caches userUri/orgUri, unwraps resource/collection, and maps to your own DTOs. Everything downstream then deals with EventType objects, not ['resource' => ['uri' => ..., 'scheduling_url' => ...]].

Scheduled sync, not live reads. Pull scheduled_events on a schedule (every 5–15 min) into a local table keyed by event URI, then page invitees per event. Use min_time/max_time on every call — omitting them forces Calendly to scan a wide window and is noticeably slower. Persist pagination.next_page_token between runs so a large backfill can resume instead of restarting.

$page = Calendly::scheduledEvents()->list([
    'user'     => $userUri,
    'min_time' => now()->subDays(1)->toIso8601ZuluString(),
    'max_time' => now()->addDays(60)->toIso8601ZuluString(),
    'status'   => 'active',
    'count'    => 100,
]);

while ($page['pagination']['next_page_token'] ?? null) {
    $page = Calendly::scheduledEvents()->list([
        'user' => $userUri,
        'page_token' => $page['pagination']['next_page_token'],
        // repeat all original filters
    ]);
}

Webhook receiver shape. Register the route outside web middleware or exclude it from CSRF (Laravel 11+: ->withoutMiddleware(VerifyCsrfToken::class) in bootstrap/app.php; older: $except). The controller should verify the signature, DB::transaction insert the raw payload into a webhook_events table with a unique index on the Calendly event URI + event type, return 200 immediately, and dispatch a queued job. Calendly retries on non-2xx and on timeouts, so the unique index is your idempotency guard — do not rely on ShouldBeUnique alone.

Booking flow. The API has no "create event" endpoint. Your flow is: list event_types → read scheduling_url → redirect the user there. Availability endpoints (available_times, user_busy_times) are for rendering slots in your own UI, but the actual booking still happens on Calendly's hosted page unless you're on a paid plan using Calendly's embedded widget. Do not build a custom booking form expecting this package to submit it.

Multi-tenant. Because the token is config-driven, the default binding is a singleton for one account. For multi-tenant, register the client as scoped() and resolve a fresh instance per tenant with config(['calendly.token' => $tenant->calendly_token]) inside the request/job scope — or construct the client directly with an explicit token. Never let a queued job pick up a token from ambient config that another job mutated.

Testing. Fake at the HTTP layer (Http::fake([...])) rather than mocking the client — you get request-shape assertions for free and it catches the URI-vs-UUID mistakes early.

Gotchas and Tips

  • URI vs UUID. Endpoints that return resources give you a full URI; endpoints that accept a single identifier usually want the UUID segment, but list filters want the full URI. Cancellation and single-event lookups take the UUID from Str::afterLast($uri, '/'). Mixed this up constantly.
  • Signature verification is on you. The release ships webhook subscription management, not payload verification. Calendly sends Calendly-Webhook-Signature: t=<ts>,v1=<hmac>, where the HMAC is SHA256 over "{$t}.{$rawBody}" using the signing_key you supplied at subscription time. Verify with hash_equals and reject timestamps older than ~5 minutes. Pass signing_key when creating the subscription or you cannot verify anything.
  • Scope matters. Webhook subscriptions require organization plus scope (user or organization). A user-scoped subscription only fires for that user's events; org-scoped requires an org admin token. Getting a 403 here is almost always a scope/token mismatch, not a code bug.
  • Cancellation returns 202, not 200. The cancellation is accepted asynchronously. Don't treat an empty body or a non-2xx-looking response as failure; check for 202 and the resource containing the cancellation. reason is a required field.
  • Pagination is manual everywhere. count maxes at 100. There is no ->paginate() helper and no auto-follow.
  • Rate limits. Calendly returns 429 with rate-limit headers. Wrap the client in a retry with exponential backoff and jitter — a naive ->retry(3, 100) on a fan-out sync will just re-trigger the limit. Cap concurrency on sync jobs to 1–2.
  • Timestamps. start_time/end_time come back UTC with a Z. Invitee timezones are separate fields. Store UTC, format at the edge; do not Carbon::parse() without ->utc() if your app timezone is set.
  • event_types needs an owner. Either user or organization, never neither. Same for scheduled_events.
  • 1.0.0 means no API stability contract yet. Pin the exact version in composer.json. The wrapper's method names and return shapes are the most likely thing to churn.
  • Config caching. php artisan config:cache in deploy will freeze the token — fine for single-tenant, fatal for the per-tenant pattern above. Use env() only inside the published config file, and resolve tenant tokens at runtime, not from config.
  • Local webhooks need a public URL. valet share / ngrok / expose; Calendly will not POST to localhost. Create the subscription with the tunnel URL, and delete it afterwards — leftover subscriptions count against the org limit.
  • Extension point: if the package exposes the underlying PendingRequest/HTTP client, prefer a macro or a wrapping service over forking. If it doesn't, that's the first thing to ask for upstream — you will want to inject headers, logging, and retry policy.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk