Replaying events from SavvyCal into your webhook with Elixir and HTTPoison
Tags: elixir, httpoison, savvycal, webhook
Published: 2023-02-18 17:56:41

This post will focus on a specific problem solution around the use of SavvyCal for developers but each step will use standard tech (for Elixir developers) and should be understandable and generalizable to other problems.

## Context

For some context, SavvyCal is a calendar booking application that allows users to publicly post availability windows and allow sign-ups. When a user books a session it goes through their workflow and ultimately ends up on whichever calendar they already use. For example, it allows anyone to book time with me for a coffee chat that will then show up on my existing Google calendar. (Try it! https://savvycal.com/byronsalty/coffee)

## Problem

While Savvy is rather full featured in their domain of calendar booking, you may want to use a webbhook to be notified of events in other systems and build other integrations. 

Let's say every time someone books you for an event you want to store their name and email in a "contacts" list that you setup. After you create that it's going to work perfectly (of course!) going forward but what about all of the bookings that happened before the webhook was operational?

_Let's figure out how to replay all the events that happened before the webhook was running by manually sending them back into the webhook_

## Outline

1. Connect to SavvyCal API to get events to "replay"
1. Create an Elixir project to act as our replay engine.
1. Convert API results into new posts to our webhook
1. Success



## 1. Connect to SavvyCal API to get events to "replay"

Savvy allows for two types of integrations - apps and personal use. We are going to use the much simpler "personal use" route.

Go to:
**Savvy Cal > Settings > Developers > Personal Tokens > Create**

Create one and write down the values that should look like:

```
pt_01GSJPE5R6N<snip>
pt_secret_d69cc128<snip>
```

[SavvyCal API Documentation](https://savvycal.com/docs/api/)

Now let's test it using `curl`.

The `me` api will give you some basic info on yourself:
```
curl -H 'Authorization: Bearer <private key>' https://api.savvycal.com/v1/me
```

We will use the `events` api to get the events that we want to replay:

```
curl https://api.savvycal.com/v1/events \
     -H 'Authorization: Bearer <private key>' \
     -H 'Content-Type: application/json'
```

So far so good?

## 2. Create an Elixir project to act as our replay engine.

We'll use `mix` to generate a base project called _replay_.

Create project:

```
$ mix new replay
$ cd replay
```

Add `HTTPoison` and `Poison` dependencies for http calls and json manipulation respectively:
```
# edit mix.exs

defp deps do
[
	{:httpoison, "~> 1.8"},
	{:poison, "~> 5.0"}
]
end
```

Install the dependencies:
```
mix deps.get
mix deps.compile
```

Test that everything is working so far:
```
$ iex -S mix

iex> response = HTTPoison.get!("http://ip.jsontest.com/")
%HTTPoison.Response{
  status_code: 200,
  ...
}

iex> json = Poison.decode!(response.body)
%{"ip" => "..."}

```

That's all the setup. Now let's put in the replay code.


## 3. Use API to get entries that we'll send to webhook

Lets a new function that we'll call from the command line to actually run our project. 

Edit `lib/replay.ex` and add a `start` function:

```
  def start do
    IO.inspect("Starting Replay...")
  end
```

Testing that everything works so far:
```
$ mix run -e "Replay.start()"
Compiling 1 file (.ex)
"Starting Replay..."
```

Now let's add the `HTTPoison` call in to retrieve events. Note that we're going to pass in `period=all` because the [API defaults to only upcoming](https://savvycal.com/docs/api/endpoints/events/#list-events) events.

```
  # adding to lib/replay.ex > start()

  savvy_api_url = "https://api.savvycal.com/v1/events?limit=100&period=all"



  headers = [
    {"Authorization", "Bearer <add private key here>"},
    {"Content-type", "application/json"}
  ]

  response = HTTPoison.get!(savvy_api_url, headers)

  json = Poison.decode!(response.body)
  entries = json["entries"]
```

Send those entries over to the webhook url that we have setup with SavvyCal:
```
  # continuing in lib/replay.ex > start()
  webook_url = "<enter your webhook url>"
  webhook_headers = [{"Content-type", "application/json"}]

  Enum.map(entries, fn entry ->
    body = Poison.encode!(%{
      type: "event.created",
      payload: entry
    })

    {:ok, _} = HTTPoison.post(webook_url, body, webhook_headers)
  end)
```

**Note:** We are going to send these events through our webhook as if they were [new events](https://savvycal.com/docs/api/webhooks/payloads) (See: `event.created`). 

**IMPORTANT: Make sure your webhook doesn't create duplicate results.** In general, you want your code to be [idempotent](https://adriennedomingus.medium.com/what-does-idempotent-mean-in-software-9bd7086b339e) so handling this is best practice in any case and will allow you to rerun this replay as many times as necessary.

## 4. Success!

For fun, let's add in some timing and then run the whole thing. I [bumped the number of entries](https://savvycal.com/docs/api/endpoints/events/#list-events) up to the max at 100. Let's see how it goes. 

We could just run our `start()` function now but I want to get some timing so I'll add a new `timed()` function that will then call our `start()` and wrap it with timing like this:
```
  #adding to lib/replay.ex > timed()
  def timed do
    {micro_secs, results} = :timer.tc( fn ->
      start()
    end)
    IO.inspect(results, label: "Num of events replayed")
    IO.inspect(micro_secs, label: "micro seconds")
  end
```

Results?
```
$ mix run -e "Replay.timed()"
Compiling 1 file (.ex)
"Starting Replay..."
Num of events replayed: 100
micro seconds: 6613082
```

6.6 seconds to replay 100 events sounds pretty good to me.