Working with Calendly webhooks in .NET

Working with Calendly webhooks in .NET

Webhooks are a simple yet effective way of integrating two systems in an event-based manner. They are beneficial to both applications as the consumer is spared from sending multiple requests checking some kind of state, and the producer does not have to waste time processing them.

That is not to say that they are always the best option - sometimes active polling is the way to go. If that's the case, make sure to check out my previous post on the topic:

Actively polling an endpoint in .NET 6
I was recently working on a document e-signing solution that had me integrating with PandaDoc. The whole process was very straightforward, but what I liked the most was that I found out about a new .NET API that I had not seen before!

Back to webhooks - while the concept is straightforward, they can differ greatly from one system to another. Some notable differences might include authentication methods, retry mechanisms and the granularity (different scopes) of the subscriptions. There's a great overview of the best practices in all areas surrounding webhooks at:

Webhooks.fyi
Learn the most popular approaches for building, securing, and operating webhooks, with recommendations for webhook providers and consumers

Some time ago, I was integrating with Calendly. If you're not familiar with the service, it's an appointment scheduling solution that allows you to share your calendar with people so that they can book a meeting with you as it fits them (and you, of course). It's a great idea because it removes all the back-and-forth of trying to find a suitable meeting time via email. Of course, there are more features available, but that's the basic premise.

Even though the integration was not that complicated, not everything was working as I expected it to. That's why I decided on sharing the code so that it might help somebody figure everything out faster than I did.

The project can be found at:

mzwierzchlewski/CalendlyEventWebhook
An example of an integration with Calendly webhooks - mzwierzchlewski/CalendlyEventWebhook

Naturally, it's not perfect by any means, but it does have some features that might prove useful. The installation and configuration guide can be found in the README.

Automatic webhook subscription creation

Thanks to the CalendlyWebhookRegistrationService, the solution will automatically create a webhook subscription in Calendly at startup. If there already exists a subscription with the same callback URL, the service will either:

  • remove the subscription if the event scope is different
  • keep the existing subscription

There's also an option to remove all the existing subscriptions regardless of their callback URLs and event scopes (CleanupAllExistingWebhooks). Of course, if you prefer to do all that manually, there's also an option to prevent the service from running altogether (SkipWebhookCreation).

Route handler registration

The project will automatically map all requests to the callback URL to a special handling middleware thanks to this extension method:

public static IEndpointRouteBuilder MapCalendlyWebhook(this IEndpointRouteBuilder endpoints)
{
    var app = endpoints.CreateApplicationBuilder();
    var calendlyConfiguration = app.ApplicationServices.GetRequiredService<CalendlyConfiguration>();
    if (string.IsNullOrEmpty(calendlyConfiguration.Webhook.CallbackUrl))
    {
        return endpoints;
    }

    var webhookUrl = new Uri(calendlyConfiguration.Webhook.CallbackUrl);
    var webhookDelegate = app.UseMiddleware<RequestHandlerMiddleware>().Build();
    endpoints.MapPost(webhookUrl.AbsolutePath, webhookDelegate);

    return endpoints;
}

So there's no need to create any controllers and no need to make sure that the route matches what's in the configuration.

Webhook request signature verification

Calendly supports a signing key parameter when creating a webhook subscription. It is strongly advised to use that parameter to make sure that the requests coming to the callback URL are really coming from Calendly. Read more about this at:

Webhook Signatures | Calendly Public API
Gain confidence in the authenticity of your webhooks when you use a webhook signing key, a unique secret key shared between your application and Calendly, to verify the events sent to your endpoints. The webhook signing key will produce the Calend...

The project includes a verification mechanism that will automatically reject any message with an invalid signature header.

Event rescheduling handling improvements

When a meeting is rescheduled in Calendly, the callback URL will be called twice - once for event cancellation and a second time for event creation.

The solution will only handle the cancellation event as it contains all the information necessary - the old event identifier, a rescheduled flag and the new event identifier (even though it is hidden in the new_invitee URI). The second event will simply be ignored because there's no need to handle a rescheduling twice.

Figuring this out took me a while, but in the end, the logic is actually dead simple:

public async Task<bool> Process()
{
    var dto = await _requestContentAccessor.GetDto();

    return (dto.Event, IsRescheduling(dto)) switch
    {
        (Event.EventCreated, false)   => await ProcessEventCreation(dto),
        (Event.EventCancelled, true)  => await ProcessEventRescheduling(dto),
        (Event.EventCancelled, false) => await ProcessEventCancellation(dto),
        _                             => true,
    };
}

private static bool IsRescheduling(WebhookDto dto)
{
    if (dto.Event == Event.EventCancelled && dto.Payload.Rescheduled)
    {
        return true;
    }

    if (dto.Event == Event.EventCreated && !string.IsNullOrEmpty(dto.Payload.OldInviteeUri))
    {
        return true;
    }

    return false;
}

private async Task<bool> ProcessEventRescheduling(WebhookDto dto)
{
    var oldId = _calendlyIdService.GetIdFromEventUri(dto.Payload.EventUri);
    var newId = _calendlyIdService.GetEventIdFromInviteeUri(dto.Payload.NewInviteeUri);
    var newEventDetails = await _calendlyService.GetEventDetails(newId.Id);

    return await _eventReschedulingHandler.Handle(oldId, newEventDetails);
}

Cover photo by Jamie Matociños on Unsplash

Show Comments