HOME/Articles/

Reversing Resy's API to create a javascript client

Article Outline

A couple months ago I made a post about building an OpenTable bot, which lets me monitor and will automatically book reservations that are hard to get or currently sold out. Well, recently I wanted to go to Le Bernardin (in New York) and Amex wasn't able to help, so I thought it would be a good reason to build something similar for Resy.

Luckily Resy's APIs were fairly straightforward - their web and mobile APIs were very similar, an didn't seem to have any rate limiting.

Notifications

Resy already supports "priority notifications", where you can sign up for notifications for specific restaurants.

{% include image.html file="resy-notify" alt="Resys notification popup" %}

However, there are a few issues with this.

1) You have to manually set these up for each date you're interested in

2) These won't automatically book them for you - if availability comes up when you're not near a device to book it, you'll probably miss out

3) I'm not sure how quickly they actually push these out. I think there's some delay to allow their concierge team to nab reservations first

I wanted to build a full end to end flow which will make both notify you that a reservation is available and book it for you.

Clients

I started by checking out their front end code and trying to de-minify it.

Interestingly, the webpack config Resy is using isn't mangling their code or removing comments, so it makes directly using their client fairly straightforward. You can rip code segments like these almost one to one.

getReservations(params) {
  return RequestBuilder.get({
    endpoint: "4/find",
    data: {
      lat: 0,
      long: 0,
      day: params.day,
      party_size: params.party_size,
      venue_id: params.venue_id,
      resy_token: params.resy_token,
    },
  }).then((response) => ResyTransformer.finder4(response));
},

There was also some classic hacky code, like specifically excluding some assets from the UI when they match a property ID.

function getServiceTypes(serviceTypes, venue, venueId) {
    // a default for a service type that is not an available service type for the venue
    const temp = [{
        available: []
    }];
    serviceTypes.forEach(service=>{
        // EXCEPTIONS - lol
        // Noble Experiment doesn't serve dinner.
        if (venueId === 569 && service.value === 'dinner') {
            temp[temp.length - 1].serviceTypeName = 'Cocktails';
        }
    }
    );
    // Robert Sinskey Vineyards doesn't serve food either (885)
    // and one more time because europe The Mulwray UK (2040)
    if (venueId === 885 || venueId === 2040) {
        temp.forEach(item=>{
            item.serviceTypeName = '';
        }
        );
    }
    return temp;
}

and some nice copy + paste

// shamelessly stolen from https://toddmotto.com/understanding-javascript-types-and-reliable-type-checking/
[
  "Array",
  "Object",
  "String",
  "Date",
  "RegExp",
  "Function",
  "Boolean",
  "Number",
  "Null",
  "Undefined",
  "Window",
].forEach((type) => {
  Utils["is".concat(type)] = (
    (self) => (elem) =>
      Object.prototype.toString.call(elem).slice(8, -1) === self
  )(type);
});

Their API is pretty full featured and clear, though, and understanding how their availability and searching worked was easy.

{% include image.html file="resy-network" alt="Resys network response" %}

Hooking it all up

I wrote a few small services for wrapping their code and logging the user in to get their authentication token, as well as for sending a text message with twilio. This will refresh my auth every day, and fetch availability every 5 minutes.

const refreshAvailability = async () => {
  log.info("Finding reservations");

  await venuesService.init();
  const venuesToSearchFor = await venuesService.getWatchedVenues();
  // You get more availability if you have an amex card and you log in

  for (const venue of venuesToSearchFor) {
    await refreshAvailabilityForVenue(venue);
  }
  await venuesService.save();
  log.info("Finished finding reservations");
};

const regenerateHeaders = async () => {
  await service.generateHeadersAndLogin();
};
// every day fetch every post
cron.scheduleJob("*/5 * * * *", refreshAvailability);
cron.scheduleJob("1 * * * *", regenerateHeaders);

regenerateHeaders().then(() => {
  refreshAvailability();
});

And then the logic for actually understanding if the dates work

const refreshAvailabilityForVenue = async (venue: VenueToWatch) => {
  try {
    const availableDates = await service.getAvailableDatesForVenue(
      venue.id,
      venue.partySize
    );
    if (!availableDates.length) {
      return;
    }
    for (const dateToCheck of availableDates) {
      const slots = (await service.getAvailableTimesForVenueAndDate(
        venue.id,
        dateToCheck.date,
        venue.partySize
      )) as EnhancedSlot[];

      const possibleSlots = slots.filter((slot) => {
        const start = dayjs(slot.date.start);
        const minTime = dayjs(`${start.format("YYYY-MM-DD")} ${venue.minTime}`);
        const maxTime = dayjs(`${start.format("YYYY-MM-DD")} ${venue.maxTime}`);
        slot.start = start;
        return start >= minTime && start <= maxTime;
      });

      if (possibleSlots.length) {
        await parsePossibleSlots(venue, possibleSlots);
        return;
      }
    }
    log.debug(`Found no valid slots for ${venue.name}`);
  } catch (e) {
    console.error(e);
  }
};

Setting up the monitoring

I wanted some form of persistence so that if a restaurant released a lot of availability all at once my script wouldn't rebook the same restaurant over and over. I used lowdb, a small JSON based "database" that stores your data in a file. I wrote a simple schema that you fill out, like what your time ranges are and what your preferred time is, and whether it should attempt to book it for you.

{
  "venues": [
    {
      "name": "Le Bernardin",
      "id": 1387,
      "notified": true,
      "minTime": "18:00",
      "preferredTime": "19:30",
      "maxTime": "22:00",
      "shouldBook": true,
      "uuid": "7088f322-578d-4b10-816f-44c4213118dd",
      "partySize": 2
    }
  ]
}

Success

A couple days later it worked, and I got a booking at Le Bernardin for my girlfriend and I.

{% include image.html file="resy-success" alt="Resys successful reservation at Le Bernardin" %}

Last one left to do now is Tock!

Code

The code is open source on GitHub here. It's probably in a state that a software engineer could get it working with the right environment variables, but it's not necessarily ready for anyone to be able to use it out of the box. PRs are welcome though!