NAV Navbar
graphql

Introduction

Welcome to the PeekPlus GraphQL API!

You can use our API to access thousands upon thousands of things to do, curated by us.

With the PeekPlus API you can

💥💥  One Endpoint  💥💥
https://api.peek.com/gql

Why GraphQL?

We chose GraphQL over some older standards because it is the best tool for us to expose our data to you in an elegant, self-documenting, flexible and performant way.

If you are new to GraphQL: we are confident you will never think about APIs the same way again.

If you are already familiar with GraphQL: we can feel your excitement; we're right there with you.

Smaller, Customizable Payloads

In a traditional "here is the endpoint to return activities" scenario, the original creators would include everything they felt "reasonable" - maybe something like:

Then someone comes along and makes a compelling case for exposing the location. So, for them, we add a few new fields for lat and lng.

Then someone comes along later and asks for review meta data because they want to expose it. Reasonable. So again we go and add a few new fields for the count and average_rating.

Then someone comes along and says they want a field that is actually pretty heavy. They are aware that it is heavier but are comfortable only asking for a few activities. So we add it.

Now the original users who just want the name and description are telling us the api is too slow; they were querying for a ton of activities and that heavy field is now a problem. So we hack together some query params where you can white-list fields:

http://example.com/activities?fields=name,description...

Great, but when you ask for activities, we actually want to also be able to get the tickets.. so now you are all

http://example.com/activities?fields=name,description,tickets.name...

..and everything starts to feel really dirty.

With GraphQL, we simply expose our object graph:

And you, the owner of your experience, ask for what you need and we return it to you. Simple as that. Furthermore, if you find your self wanting 4 or 5 different things, you can ask for them all at once saving many round trips to the server.

Wins. All. Around.

Strongly Typed

With a traditional REST API, you find yourself stringing together complex query params to filter content:

http://example.com/activities?sort=price&filter[state]=NY...

Inevitably the question comes in "what are possible values for SORT", or "what should I use for the STATE?". In those traditional APIs, you'd be digging for the email that linked to the docs or some light guessing and checking. It is then up to the api server to gracefully handle all possible mistakes to get you a reasonable error message if you deviate from what's expected. This is a lot of work for both parties, and is error prone.

With GraphQL, every query is Strongly Typed - you get immediate feedback of what the value can be, and if you deviate from it, the request doesn't even make it to our application logic; you are immediately given a good error explaining your mistake.

My client is catching a typo for me:

image

Self Documenting

Piggy-backing on the strongly type point from above, GraphQL was written from the ground up to be easy to "introspect". There are a host of great free clients out there; we are big fans of https://insomnia.rest/graphql/ - simply by downloading that client and pointing to our sandbox url, you have every query, field and mutation documented. We find crafting your queries there and then moving them into your app is a solid way to work.

image

Further Reading

Authentication

JWT

When you are granted access to the API, you will be given an Api Key ("your-api-key") and a Secret ("your-secret"). Every request you make will require you to create a JWT that includes your API Key in the sub field and a unique ID in the jti field, which is then signed w/ your secret:

{"sub": "your-api-key", "jti": "unique-id", "exp": 1516239022}

If you're new to JWTs, https://jwt.io/ is a GREAT place to start:

image

defmodule Token do
  use Joken.Config

  @doc """
  Generate a signed token w/ default claims
  """
  def generate do
    {:ok, claims} =
      # NOTE: Joken automatically adds a uniq `jti` + `exp` for every token generated.
      Token.generate_claims(%{
        "sub" => "your-api-key"
      })

    {:ok, token, _claims} = encode_and_sign(claims, signer())

    token
  end

  defp signer, do: Joken.Signer.create("HS256", "your-secret")
end

Token.generate()

The reason we chose JWT is because there exists a client for (nearly?) every language imaginable. No need to write your own logic, just find the library (all listed on the above site) for your language and it should be straightforward from there.

We are an Elixir Shop, so our library of choice is called Joken; on the right is how you might choose to implement the above w/ Joken.

Add Token as Header

The Token you generated above has to be included in the header for every request:

Authorization: Bearer TOKEN

Idempotency

Since every request that you make must contain a unique request id inside the signed token, if you make a second request with the same request id we will assume this you replaying a request and will return the original response.

Versioning

Another advantage of GraphQL is that the need for versioning largely goes away; the schema evolves overtime without the need for strict versions.

Backwards Compatible Changes

99% of our GraphQL changes are backwards compatible; they involve deprecating a field which just results in a warning if used but doesn't stop working. Or it's a new field altogether and since you aren't asking for it, it doesn't affect you.

image

Breaking Changes

We do everything possible to avoid these, and at the time of writing this, see no reason why we'll ever have them. That said, let's say a field has been deprecated for 6 months and an integration continues to use it even after being told that it's going away, eventually that field will have to go, which yes, would be a breaking change. Beyond this we don't see a reason why we can't evolve this API through clean deprecations. We will notify all clients using deprecated fields of the potential breaking change and give ample time to update before removing completely.

Entities

Activity

An Activity represents something people do to have fun. An Activity have a list of dates, the date when an instance of an Activity occurs. And, each date, have a list of times, the specific times for the given date when the activity starts and ends.

Attributes

Attribute Description
id id
The unique identifier for the object.
name string
The name of the activity.
dates array<AvailabiltyDate>
List of dates when the activity occurs.
tickets array<Ticket>
List of the supported tickets types for the activity.
requiresAdult boolean
Indicates if the quote must have 1 Adult or 1 Senior or 1 Traveler ticket types along the selected tickets sent.
minTravelersPerBooking integer
Regarding all the ticket types and quantities selected, you must, at least, have this number of travelers selected for the booking.
maxTravelersPerBooking integer
Regarding all the ticket types and quantities selected, you must, at most, have this number of travelers selected for the booking.

ActivityVariant

An ActivityVariant is a variation for the Activity that represents little differences between the original activity.

Any particular activity may consist of a number of variants, each of which might represent different departure times, tour routes, or packaged extras like additional meals, transport and so forth.

Attributes

Attribute Description
id id
The unique identifier for the object.
title string
Human readable text with the short name of the variant.
description string
Human readable text that contains specific information and details about the variant.

ActivityVariantQuestion

An ActivityVariantQuestion is a booking question that may be required to answer abd represents vital information specified by the supplier about the individual travelers or the group as a whole to be sent to the supplier as part of the request in order to complete a booking.

Implementations (or Types)

Type name
StringQuestion
DateQuestion
TimeQuestion
SingleChoiceQuestion
LocationQuestion
NumberAndUnitQuestion

Attributes

Attribute Description
id id
The unique identifier for the question. This must be sent with the answers when booking an activity.
required boolean
Indicates if answering the question is required (true) or not (false).
group array<ActivityVariantQuestionGroup>
Indicates how the question should be answered: once PER_TRAVELER or once PER_BOOKING.
hint string
A string that contains information on how the field is expected to be answered. It may be not present.
label non-null string
A string that contains the human readable name of the question being asked. Examples: First Name, Last Name, Pickup Location, etc.

Other fields may be present depending on the question type. These are the specific fields for each question type:

StringQuestion

Attributes

Attribute Description
max_length integer
The maximum allowed length for the answer to this question.

Validation

The answer to this question must be a string that have at most max_length in size.

DateQuestion

Attributes

no other attributes present

Validation

The answer to this question must be a valid string representing a "Date" in the format described by ISO 8601:2019.

TimeQuestion

no other attributes present

Validation

The answer to this question must be a valid string representing a "Local time" in the format described by ISO 8601:2019.

SingleChoiceQuestion

Attributes

Attribute Description
options array<ActivityVariantQuestionOption>
The unique identifier for the question. This must be sent with the answers when booking an activity.

Validation

The answer to this question must be a string that is contained within the provided options value attribute.

LocationQuestion

Attributes

Attribute Description
max_length integer
The maximum allowed length for the answer to this question.
options array<ActivityVariantQuestionOption>
The unique identifier for the question. This must be sent with the answers when booking an activity.

Validation

The answer to this question must be a string that is contained within the provided options value attribute.

NumberAndUnitQuestion

Attributes

Attribute Description
units array<string>
List of units for the question. The selected unit must be sent together with the answer for this type of question.

Validation

The answer to this question must be a string that correctly represents a number (1000, 105.56, 0.1234 are valid numbers) and the unit must be one of the values contained withing the units array.

ActivityVariantQuestionGroup

Indicates how the question should be answered: once PER_TRAVELER or once PER_BOOKING.

Values

Value Description
PER_BOOKING Indicates that the answer to the question must be answered for each traveler, i.e., answered once per ticket added to the quote .
PER_TRAVELER Indicates that the answer to the question must be answered for the booked group as a whole, i.e., answered once per booking.

ActivityVariantQuestionOption

An ActivityVariantQuestionOption represents a possible answer for a given question inside a list of possible answers (or options). Each option may have a list of followup questions, i.e. questions that must be answered for a given answer to the parent question of this option.

Attributes

Attribute Description
name string
Human readable text that indicates the value of this option.
value string
The actual value for the option that must be sent as an answer for the parent question.
questions array<ActivityVariantQuestion>
A list of follow up questions for the option, the may or may not be required to answer (based on each follow up question required attribute)

AvailabilityDate

An AvailabilityDate is the representation of a date when an Activity occurs. For a given date, multiple AvailabilityTime can be related to the same date, indicating possible start and end times for the activity on the given date.

Attributes

Attribute Description
date string
Human readable text that indicates the value of this option.
isAvailable string
The actual value for the option that must be sent as an answer for the parent question.
times array<AvailabilityTime>
A list of follow up questions for the option, the may or may not be required to answer (based on each follow up question required attribute)

AvailabilityTime

An AvailabilityTime represents a start and an end time for a given date when the parent Activity happens.

Attributes

Attribute Description
id string
The unique identifier for this object.
localDateTimeStart string
NaiveDateTime ISO8601 formatted string that indicates which date and time the activity starts, without timezone.
localDateTimeEnd string
NaiveDateTime ISO8601 formatted string that indicates which date and time the activity ends, without timezone.
status AvailabilityTimeStatus
If the given time is available and if not, why.
variant ActivityVariant
The activity variant that have the availability for this specific time.

AvailabilityTimeStatus

The status of a given AvailabilityTime.

Values

Value Description
AVAILABLE Currently available for sale, but has a fixed capacity.
CLOSED Currently not available for sale, but not sold out (e.g. temporarily on stop-sell) and may be available for sale soon.
FREESALE Always available.
LIMITED Currently available for sale, but has a fixed capacity and may be sold out soon.
SOLD_OUT Currently sold out, but additional availability may free up.

CancellationReason

The reason for the cancellation.

Values

Value Description
BOOKED_WRONG_TOUR_DATE Booked the wrong tour date.
CANCELED_ENTIRE_TRIP Cancelled the entire trip.
CHOSE_A_DIFFERENT_CHEAPER_TOUR Chose a different but cheaper tour.
DUPLICATE_BOOKING Duplicate booking.
SIGNIFICANT_GLOBAL_EVENT Significant global event.
SUPPLIER_NO_SHOW Supplier no show.
UNEXPECTED_MEDICAL_CIRCUMSTANCES Unexpected medical circumstances.
WEATHER Weather.

Ticket

A Ticket represents the supported ticket type for a given Activity, which may have different configurations and prices supported by the activity provider. They can be Adult, Child, Traveler, etc

Attributes

Attribute Description
id string
The unique identifier for this object.
name string
The name that represents the ticket, like Adult, Child, Traveler, etc.
minQuantity integer
The minimum number of this ticket that can be booked at once.
maxQuantity integer
The maximum number of this ticket that can be booked at once.

Queries

As mentioned earlier, the source of truth for what's available can be browsed from directly inside your GraphQL Client. Duplicating and maintaining the spec would defeat the advantages of GraphQL. With that in mind, this section will be very high level to help shape your understanding of what's available, not the source of truth.

Activities

The Activities query is the most powerful query in the API in that it can be used for two major use-cases:

  1. Provide activities based on filters/sorting (location / price / keywords / etc). This could power real-time experiences where you need very specific activities for a given use-case.
  2. You want to "ingest" our entire inventory through crawling every page of the results, pulling every field that is of importance to you.
query Activities($filter: ActivitiesFilter!, $pagination: Pagination!, $sort: ActivitySort) {
  activities(filter: $filter, pagination: $pagination, sort: $sort) {
    entries {
      name
    }
  }
}

For the purposes of this example, let's say you just need each Activity's name. Your query for both use-cases would be identical.

In the two examples above, the main difference is what you pass in for variables. For a very specific query you'd pass in filters and sort values based on your desires. For crawling the whole inventory, you'd also likely not include any filters and you'd ask for totalPages so you can crawl through all of them, etc.

Example Response:

{
  "activities": [{
    "entries": [
      { "name": "Activity 1" },
      { "name": "Activity 2" },
    ]
  }]
}

Availability

Determining whether something is available can be thought of as a two part process.

  1. Get high-level cacheable dates + times to get an idea of, roughly speaking, when a given activity is available.
  2. Given a specific set of tickets (1x Adult, 2x Child) for a specific time, is it available and what will the price be.

The first is what we're going to talk about here. The second is in the next section as we call that a BookingQuote Mutation.

See ActivityVariants.

query DocExampleGetActivityById($id: ID!, $startDate: Date!, $endDate: Date!, $quantity: Int) {
  activity(id: $id) {
    name
    dates(startDate:$startDate, endDate: $endDate, quantity: $quantity) {
      date
      times {
        id
        localDateTimeStart
        localDateTimeEnd
        status
        variant {
          id
          title
          description
        }
      }
    }
  }
}

With GraphQL, pulling availability is simply a "field" you can request for an activity.

Notice with GraphQL, "fields" can accept arguments (in this case the desired availability).

The power of the above is that you can do the exact same thing with a search result and in a single query to the server, answer the question "What are the top 10 things to do close by and what dates/times are available for them".

ActivityVariants

An ActivityVariant is a variation for the Activity that represents little differences between the original activity.

Any particular activity may consist of a number of variants, each of which might represent different departure times, tour routes, or packaged extras like additional meals, transport and so forth.

Attributes

Attribute Description
id id
The unique identifier for the activity variant.
title string
Human readable text with the short name of the variant.
description string
Human readable text that contains specific information and details about the variant.
fragment SimpleOptionsFields on ActivityQuestionOption {
    name
    value
}

fragment CompleteOptionsFields on ActivityQuestionOption {
    name
    questions {
        __typename
        id
        group
        required
        label
        hint

        ... on NumberAndUnitQuestion {
            units
        }

        ... on LocationQuestion {
            allowCustomPickup
            maxLength
            options {
                ...SimpleOptionsFields
            }
        }

        ... on SingleChoiceQuestion {
            options {
                ...SimpleOptionsFields
            }
        }

        ... on StringQuestion {
            maxLength
        }
    }
    value
}

query DocExampleGetActivityVariantById($id: ID!) {
    activityVariant(id: $id) {
        ... on ActivityVariant {
            title
            description
            questions {
                __typename
                id
                group
                required
                label
                hint

                ... on NumberAndUnitQuestion {
                    units
                }

                ... on LocationQuestion {
                    allowCustomPickup
                    maxLength
                    options {
                        ...SimpleOptionsFields
                    }
                }

                ... on SingleChoiceQuestion {
                    options {
                        ...CompleteOptionsFields
                    }
                }

                ... on StringQuestion {
                    maxLength
                }
            }
        }
    }
}

CreateBookingQuote

A quote holds a set of inventory while locking in a price. You can think of a quote as a draft booking or a reservation.

There are times when something you thought would be available based on the Availability Calls is not available when making the quote.

This can happen for a few reasons:

1) The availability call was out of sync. Given this makes a real-time call to check the supplier's inventory, there's always a window where our initial call returned availability but this call didn't. Our goal is to make this window as small as possible

2) Certain suppliers have very complex rules on how they accept bookings. It might be that your specific tickets aren't available, or there are complex rules around time-cutoffs.

3) There are ticket rules to respect when asking for a quote. The specific activity and ticket fields are described below and how they affect the mutation result:

Activity

Attribute Description
requiresAdult boolean
Indicates if the quote must have 1 Adult or 1 Senior or 1 Traveler ticket types along the selected tickets sent.
maxTravelersPerBooking integer
Regarding all the ticket types and quantities selected, you must, at least, have this number of travelers selected for the booking.
minTravelersPerBooking integer
Regarding all the ticket types and quantities selected, you must, at most, have this number of travelers selected for the booking.

Ticket

Attribute Description
minQuantity integer
The minimum number of this ticket that can be booked at once.
maxQuantity integer
The maximum number of this ticket that can be booked at once.

With those caveats aside, making these requests might look like this:

mutation CreateQuote($request: BookingQuoteInput!) {
  createBookingQuote(quoteInput: $request) {
    __typename
    ... on BookingQuote {
      id
      priceBreakdown {
        price {
          formatted
        }
        total {
          formatted
        }
        taxes {
          formatted
        }
      }
      snapshot {
        activity {
          variant {
            id
            title
          }
        }
      }
    }
    ... on Error {
      message
    }
  }
}

Request variables

{
  "request": {
    "availabilityTimeId": "<available-time-id>",
    "tickets": [
      {
        "quantity": 4,
        "ticketId": "<ticket-id>"
      }
    ]
  }
}

And a Response like this

{
  "data": {
    "createBookingQuote": {
      "__typename": "BookingQuote",
      "id": "<booking-quote-id>",
      "priceBreakdown": {
        "price": {
          "formatted": "$620.00"
        },
        "taxes": {
          "formatted": "$0.00"
        },
        "total": {
          "formatted": "$620.00"
        }
      },
      "snapshot": {
        "activity": {
          "variant": {
            "id": "<variant-id>",
            "title": "Solvang Food and Photo Tour"
          }
        }
      }
    }
  }
}

CreateBookingQuote Errors

BadRequestError

Given the provided activity properties on the side and summarized below, we will show different inputs with values that do not match the activity and/or tickets requirements, with the according error messages for each one:

Activity Response

{
  "data": {
    "activities": {
      "entries": [
        {
          "id": "<activity-id>",
          "maxTravelersPerBooking": 10,
          "minTravelersPerBooking": 2,
          "name": "Solvang Food & Photo Tour: Explore Danish Cuisine & Photography in Santa Ynez Valley",
          "requiresAdult": true,
          "tickets": [
            {
              "id": "<adult-ticket-id>",
              "maxQuantity": 10,
              "minQuantity": 1,
              "name": "Adult"
            },
            {
              "id": "<youth-ticket-id>",
              "maxQuantity": 4,
              "minQuantity": 0,
              "name": "Youth"
            }
          ]
        }
      ],
      "pageNumber": 1,
      "pageSize": 200,
      "totalEntries": 1,
      "totalPages": 1
    }
  }
}

Activity

Attribute Value
requiresAdult true
minTravelersPerBooking 2
maxTravelersPerBooking 10

Adult Ticket

Attribute Value
minQuantity 1
maxQuantity 10

Youth Ticket

Attribute Value
minQuantity 0
maxQuantity 4

"Requires at least one adult or senior"

When it happens?

Not providing the ADULT ticket, only the YOUTH ticket.

requiresAdult true
Sending ADULT ticket in request false
Range minTravelersPerBooking - maxTravelersPerBooking 2 - 10
Quantity sent on Request 2

Adult Ticket

Range minQuantity - maxQuantity 1 - 10
Quantity sent on Request 0

Youth Ticket

Range minQuantity - maxQuantity 0 - 4
Quantity sent on Request 2

Request input

{
  "request": {
    "availabilityTimeId": "<available-time-id>",
    "tickets": [
      {
        "quantity": 2,
        "ticketId": "<youth-ticket-id>"
      }
    ]
  }
}

Response

{
  "data": {
    "createBookingQuote": {
      "__typename": "BadRequestError",
      "message": "Requires at least one adult or senior"
    }
  }
}

"Minimum # of guests is <num> and requires at least one adult or senior"

When it happens?

Not providing the ADULT ticket and not meeting the minTravelersPerBooking for the activity.

requiresAdult true
Sending ADULT ticket in request false
Range minTravelersPerBooking - maxTravelersPerBooking 2 - 10
Quantity sent on Request 1

Adult Ticket

Range minQuantity - maxQuantity 1 - 10
Quantity sent on Request 0

Youth Ticket

Range minQuantity - maxQuantity 0 - 4
Quantity sent on Request 1

Request input

{
  "request": {
    "availabilityTimeId": "<available-time-id>",
    "tickets": [
      {
        "quantity": 1,
        "ticketId": "<youth-ticket-id>"
      }
    ]
  }
}

Response

{
  "data": {
    "createBookingQuote": {
      "__typename": "BadRequestError",
      "message": "Minimum # of guests is 2 and requires at least one adult or senior"
    }
  }
}

"Minimum # of guests for ticket '<ticket-id>' is <num>"

When it happens?

Providing the ADULT ticket, the YOUTH ticket and not meeting the minQuantity for the YOUTH ticket.

requiresAdult true
Sending ADULT ticket in request true
Range minTravelersPerBooking - maxTravelersPerBooking 3 - 10
Quantity sent on Request 2

Adult Ticket

Range minQuantity - maxQuantity 1 - 10
Quantity sent on Request 1

Youth Ticket

Range minQuantity - maxQuantity 2 - 4
Quantity sent on Request 1

Request input

{
  "request": {
    "availabilityTimeId": "<available-time-id>",
    "tickets": [
      {
        "quantity": 1,
        "ticketId": "<adult-ticket-id>"
      },
      {
        "quantity": 1,
        "ticketId": "<youth-ticket-id>"
      }
    ]
  }
}

Response

{
  "data": {
    "createBookingQuote": {
      "__typename": "BadRequestError",
      "message": "Minimum # of guests for ticket '<youth-ticket-id>' is 2"
    }
  }
}

"Maximum # of guests for ticket '<ticket-id>' is <num>"

When it happens?

Providing the ADULT ticket, the YOUTH ticket and not meeting the maxQuantity for the YOUTH ticket.

requiresAdult true
Sending ADULT ticket in request true
Range minTravelersPerBooking - maxTravelersPerBooking 2 - 10
Quantity sent on Request 10

Adult Ticket

Range minQuantity - maxQuantity 1 - 10
Quantity sent on Request 5

Youth Ticket

Range minQuantity - maxQuantity 0 - 4
Quantity sent on Request 5

Request input

{
  "request": {
    "availabilityTimeId": "<available-time-id>",
    "tickets": [
      {
        "quantity": 5,
        "ticketId": "<adult-ticket-id>"
      },
      {
        "quantity": 5,
        "ticketId": "<youth-ticket-id>"
      }
    ]
  }
}

Response

{
  "data": {
    "createBookingQuote": {
      "__typename": "BadRequestError",
      "message": "Maximum # of guests for ticket '<youth-ticket-id>' is 4"
    }
  }
}

"Maximum # of guests is <num>"

When it happens?

Providing the ADULT ticket, the YOUTH ticket and not meeting the maxTravelersPerBooking for the activity.

requiresAdult true
Sending ADULT ticket in request true
Range minTravelersPerBooking - maxTravelersPerBooking 2 - 10
Quantity sent on Request 11

Adult Ticket

Range minQuantity - maxQuantity 1 - 10
Quantity sent on Request 7

Youth Ticket

Range minQuantity - maxQuantity 0 - 4
Quantity sent on Request 4

Request input

{
  "request": {
    "availabilityTimeId": "<available-time-id>",
    "tickets": [
      {
        "quantity": 7,
        "ticketId": "<adult-ticket-id>"
      },
      {
        "quantity": 4,
        "ticketId": "<youth-ticket-id>"
      }      
    ]
  }
}

Response

{
  "data": {
    "createBookingQuote": {
      "__typename": "BadRequestError",
      "message": "Maximum # of guests is 10"
    }
  }
}

UpdateBookingQuote

It is useful to update a quote as a customer changes the tickets, switches their desired time, add required booking question answers, etc.

This call is nearly identical to the CreateBookingQuote:

mutation EditBookingQuote($request: EditBookingQuoteInput!) {
  editBookingQuote(input: $request) {
    __typename
    ... on BookingQuote {
      id
      priceBreakdown {
        price {
          formatted
        }
        taxes {
         formatted
        }
        fees {
          formatted
        }
        total {
          formatted
        }
      }
    }
    ... on Error {
      message
    }
  }
}

Request variables

{
  "request": {
    "bookingQuoteId": "<booking-quote-id>",
    "quoteInput": {
      "availabilityTimeId": "<available-time-id>",
      "tickets": [
        {
          "quantity": 5,
          "ticketId": "<ticket-id>"
        }
      ]
    }
  }
}

Booking Questions

Some activities may require questions answers in order to be possible to book.

The questions may be

Let's consider the example on the side. You have the <available-time-id> with the <activity-variant-id> for which it belongs to. So you can query that specific ActivityVariant and see if there are questions that must be answered. See ActivityVariants do recall the query that produces the json output replicated on the side.

Request variables

{
  "id": "<activity-variant-id>"
}

Response

{
  "data": {
    "activityVariant": {
      "description": "Skip the tourist traps and head out with a local foodie for a fun-filled, behind-the-scenes cultural food tour in the unique Danish village nestled in the heart of the Santa Ynez Valley (30 miles north of downtown Santa Barbara). \n\nOur Solvang food tour starts along Mission Drive and will walk you through the the downtown neighborhood streets where you will taste traditional Danish and local artisan food and drink unique to Solvang and the Central Coast. Indulge in bites of traditional Danish comfort fare and freshly baked goods as well as craft beer, local wine, & artisan eats. Along the way, you’ll get immersed in the local culture and hear interesting stories about each place you’ll visit.\n\nYou’ll also learn easy-to-shoot food photo tips from your pro tour guide on this foodie photography tour and take your photo skills to the next level using your smartphone’s camera app. By the end of your 3-4 hour tour, you’ll be eating like a local and shooting like a pro!",
      "questions": [
        {
          "__typename": "DateQuestion",
          "group": "PER_TRAVELER",
          "hint": null,
          "id": "<date-of-birth-question-id>",
          "label": "Date of birth",
          "required": true
        },
        {
          "__typename": "StringQuestion",
          "group": "PER_TRAVELER",
          "hint": null,
          "id": "<first-name-question-id>",
          "label": "First name",
          "maxLength": 50,
          "required": true
        },
        {
          "__typename": "StringQuestion",
          "group": "PER_TRAVELER",
          "hint": null,
          "id": "<last-name-question-id>",
          "label": "Last name",
          "maxLength": 50,
          "required": true
        },
        {
          "__typename": "StringQuestion",
          "group": "PER_BOOKING",
          "hint": null,
          "id": "<special-requirements-question-id>",
          "label": "Special requirements",
          "maxLength": 1000,
          "required": false
        },
        {
          "__typename": "SingleChoiceQuestion",
          "group": "PER_BOOKING",
          "hint": "Select the language you want for this tour",
          "id": "<tour-language-question-id>",
          "label": "Tour language",
          "options": [
            {
              "followupQuestions": [],
              "name": "English (Guide)",
              "value": "GUIDE|en|en/SERVICE_GUIDE"
            }
          ],
          "required": true
        }
      ]
    }
  }
}

In order to be able to book this activity, i.e., send the quote id to the createBooking mutation and it succeeds, we will need to update the quote with the answers for each type of question.

As we can see:

For the updated graphql query below (with the answers fields added for the purpose of this documentation)

mutation DocExampleEditBookingQuote($request: EditBookingQuoteInput!) {
  editBookingQuote(input: $request) {
    __typename
    ... on BookingQuote {
      id
      priceBreakdown {
        price {
          formatted
        }
        taxes {
         formatted
        }
        fees {
          formatted
        }
        total {
          formatted
        }
      }
      snapshot {
        activity {
          variant {
            id
            title
          }
          bookingAnswers {
            question
            answer
          }
        }
        tickets {
          id
          quantity
          guestsAnswers {
            guestAnswers {
              question
              answer
            }
          }
        }
      }
    }
    ... on Error {
      message
    }
  }
}

The editBooking request variables would be like this:

{
  "request": {
    "bookingQuoteId": "<booking-quote-id>",
    "quoteInput": {
      "availabilityTimeId": "<available-time-id>",
      "tickets": [
        {
          "quantity": 2,
          "ticketId": "<adult-ticket-id>",
          "guestsAnswers": [
            {
              "guestAnswers": [
                {
                  "question": "<date-of-birth-question-id>",
                  "answer": "Adult1 Date of Birth"
                },
                {
                  "question": "<first-name-question-id>",
                  "answer": "Adult1 First Name"
                },
                {
                  "question": "<last-name-question-id>",
                  "answer": "Adult1 Last Name"
                }
              ]
            },
            {
              "guestAnswers": [
                {
                  "question": "<date-of-birth-question-id>",
                  "answer": "Adult2 Date of Birth"
                },
                {
                  "question": "<first-name-question-id>",
                  "answer": "Adult2 First Name"
                },
                {
                  "question": "<last-name-question-id>",
                  "answer": "Adult2 Last Name"
                }
              ]
            }
          ]
        }
      ],
      "bookingAnswers": [
        {
          "question": "<special-requirements-question-id>",
          "answer": "No special requirements"
        },
        {
          "question": "<tour-language-question-id>",
          "answer": "GUIDE|en|en/SERVICE_GUIDE"
        }
      ]
    }
  }
}

And the Response would be like

{
  "data": {
    "editBookingQuote": {
      "__typename": "BookingQuote",
      "id": "qb098ej",
      "priceBreakdown": {
        "fees": {
          "formatted": "$0.00"
        },
        "price": {
          "formatted": "$310.00"
        },
        "taxes": {
          "formatted": "$0.00"
        },
        "total": {
          "formatted": "$310.00"
        }
      },
      "snapshot": {
        "activity": {
          "bookingAnswers": [
            {
              "answer": "No special requirements",
              "question": "<special-requirements-question-id>"
            },
            {
              "answer": "GUIDE|en|en/SERVICE_GUIDE",
              "question": "<tour-language-question-id>"
            }
          ],
          "variant": {
            "id": "<activity-variant-id>",
            "title": "Solvang Food and Photo Tour"
          }
        },
        "tickets": [
          {
            "guestsAnswers": [
              [
                {
                  "guestAnswers": [
                    {
                      "answer": "Adult1 Date of Birth",
                      "question": "<date-of-birth-question-id>"
                    },
                    {
                      "answer": "Adult1 First Name",
                      "question": "<first-name-question-id>"
                    },
                    {
                      "answer": "Adult1 Last Name",
                      "question": "<last-name-question-id>"
                    }
                  ]
                }
              ],
              [
                {
                  "guestAnswers": [
                    {
                      "answer": "Adult2 Date of Birth",
                      "question": "<date-of-birth-question-id>"
                    },
                    {
                      "answer": "Adult2 First Name",
                      "question": "<first-name-question-id>"
                    },
                    {
                      "answer": "Adult2 Last Name",
                      "question": "<last-name-question-id>"
                    }
                  ]
                }
              ]
            ],
            "id": "<adult-ticket-id>",
            "quantity": 2
          }
        ]
      }
    }
  }
}

UpdateBookingQuote Errors

BadRequestError

Given the provided activity properties on the side and summarized below, we will show different inputs with values that do not match the activity and/or tickets requirements, with the according error messages for each one:

Activity Response

{
  "data": {
    "activities": {
      "entries": [
        {
          "id": "<activity-id>",
          "maxTravelersPerBooking": 10,
          "minTravelersPerBooking": 2,
          "name": "Solvang Food & Photo Tour: Explore Danish Cuisine & Photography in Santa Ynez Valley",
          "requiresAdult": true,
          "tickets": [
            {
              "id": "<adult-ticket-id>",
              "maxQuantity": 10,
              "minQuantity": 1,
              "name": "Adult"
            },
            {
              "id": "<youth-ticket-id>",
              "maxQuantity": 4,
              "minQuantity": 0,
              "name": "Youth"
            }
          ]
        }
      ],
      "pageNumber": 1,
      "pageSize": 200,
      "totalEntries": 1,
      "totalPages": 1
    }
  }
}

Activity

Attribute Value
requiresAdult true
minTravelersPerBooking 2
maxTravelersPerBooking 10

Adult Ticket

Attribute Value
minQuantity 1
maxQuantity 10

Youth Ticket

Attribute Value
minQuantity 0
maxQuantity 4

"Requires at least one adult or senior"

When it happens?

Not providing the ADULT ticket, only the YOUTH ticket.

Activity

requiresAdult true
Sending ADULT ticket in request false
Range minTravelersPerBooking - maxTravelersPerBooking 2 - 10
Quantity sent on Request 2

Adult Ticket

Range minQuantity - maxQuantity 1 - 10
Quantity sent on Request 0

Youth Ticket

Range minQuantity - maxQuantity 0 - 4
Quantity sent on Request 2

Request input

{
  "request": {
    "bookingQuoteId": "<booking-quote-id>",
    "quoteInput": {
      "availabilityTimeId": "<available-time-id>",
      "tickets": [
        {
          "quantity": 2,
          "ticketId": "<youth-ticket-id>"
        }
      ]
    }
  }
}

Response

{
  "data": {
    "editBookingQuote": {
      "__typename": "BadRequestError",
      "message": "Requires at least one adult or senior"
    }
  }
}

"Minimum # of guests is <num> and requires at least one adult or senior"

When it happens?

Not providing the ADULT ticket and not meeting the minTravelersPerBooking for the activity.

Activity

requiresAdult true
Sending ADULT ticket in request false
Range minTravelersPerBooking - maxTravelersPerBooking 2 - 10
Quantity sent on Request 1

Adult Ticket

Range minQuantity - maxQuantity 1 - 10
Quantity sent on Request 0

Youth Ticket

Range minQuantity - maxQuantity 0 - 4
Quantity sent on Request 1

Request input

{
  "request": {
    "bookingQuoteId": "<booking-quote-id>",
    "quoteInput": {
      "availabilityTimeId": "<available-time-id>",
      "tickets": [
        {
          "quantity": 1,
          "ticketId": "<youth-ticket-id>"
        }
      ]
    }
  }
}

Response

{
  "data": {
    "editBookingQuote": {
      "__typename": "BadRequestError",
      "message": "Minimum # of guests is 2 and requires at least one adult or senior"
    }
  }
}

"Minimum # of guests for ticket '<ticket-id>' is <num>"

When it happens?

Providing the ADULT ticket, the YOUTH ticket and not meeting the minQuantity for the YOUTH ticket.

Activity

requiresAdult true
Sending ADULT ticket in request true
Range minTravelersPerBooking - maxTravelersPerBooking 3 - 10
Quantity sent on Request 2

Adult Ticket

Range minQuantity - maxQuantity 1 - 10
Quantity sent on Request 1

Youth Ticket

Range minQuantity - maxQuantity 2 - 4
Quantity sent on Request 1

Request input

{
  "request": {
    "bookingQuoteId": "<booking-quote-id>",
    "quoteInput": {
      "availabilityTimeId": "<available-time-id>",
      "tickets": [
        {
          "quantity": 1,
          "ticketId": "<adult-ticket-id>"
        },
        {
          "quantity": 1,
          "ticketId": "<youth-ticket-id>"
        }
      ]
    }
  }
}

Response

{
  "data": {
    "createBookingQuote": {
      "__typename": "BadRequestError",
      "message": "Minimum # of guests for ticket '<youth-ticket-id>' is 2"
    }
  }
}

"Maximum # of guests for ticket '<ticket-id>' is <num>"

When it happens?

Providing the ADULT ticket, the YOUTH ticket and not meeting the maxQuantity for the YOUTH ticket

Activity

requiresAdult true
Sending ADULT ticket in request true
Range minTravelersPerBooking - maxTravelersPerBooking 2 - 10
Quantity sent on Request 10

Adult Ticket

Range minQuantity - maxQuantity 1 - 10
Quantity sent on Request 5

Youth Ticket

Range minQuantity - maxQuantity 0 - 4
Quantity sent on Request 5

Request input

{
  "request": {
    "bookingQuoteId": "<booking-quote-id>",
    "quoteInput": {
      "availabilityTimeId": "<available-time-id>",
      "tickets": [
        {
          "quantity": 5,
          "ticketId": "<adult-ticket-id>"
        },
        {
          "quantity": 5,
          "ticketId": "<youth-ticket-id>"
        }
      ]
    }
  }
}

Response

{
  "data": {
    "editBookingQuote": {
      "__typename": "BadRequestError",
      "message": "Maximum # of guests for ticket '<youth-ticket-id>' is 4"
    }
  }
}

"Maximum # of guests is <num>"

When it happens?

Providing the ADULT ticket, the YOUTH ticket and not meeting the maxTravelersPerBooking for the activity

Activity

requiresAdult true
Sending ADULT ticket in request true
Range minTravelersPerBooking - maxTravelersPerBooking 2 - 10
Quantity sent on Request 11

Adult Ticket

Range minQuantity - maxQuantity 1 - 10
Quantity sent on Request 7

Youth Ticket

Range minQuantity - maxQuantity 0 - 4
Quantity sent on Request 4

Request input

{
  "request": {
    "bookingQuoteId": "<booking-quote-id>",
    "quoteInput": {
      "availabilityTimeId": "<available-time-id>",
      "tickets": [
        {
          "quantity": 7,
          "ticketId": "<adult-ticket-id>"
        },
        {
          "quantity": 4,
          "ticketId": "<youth-ticket-id>"
        }
      ]
    }
  }
}

Response

{
  "data": {
    "editBookingQuote": {
      "__typename": "BadRequestError",
      "message": "Maximum # of guests is 10"
    }
  }
}

Invalid Questions Error

If you forget to provide required questions answers or provide invalid questions answers, you will receive the InvalidQuestionsError with details about the invalid questions detected.

Request (no required questions answers provided)

{
  "request": {
    "bookingQuoteId": "<booking-quote-id>",
    "quoteInput": {
      "availabilityTimeId": "<available-time-id>",
      "tickets": [
        {
          "quantity": 2,
          "ticketId": "<adult-ticket-id>"
        }
      ],
      "bookingAnswers": [
        {
          "question": "<special-reqs-question-id>",
          "answer": "No special requirements"
        }
      ]
    }
  }
}

Response

{
  "data": {
    "createBooking": {
      "__typename": "InvalidQuestionsError",
      "message": "Missing guest answers for tickets: <ticket-id>. Missing required questions: <question1-id>, <question2-id>, ..., <questionN-id>"
    }
  }
}

CreateBooking

This will create a booking out of a provided quote. At this point, your booking is submitted and there's nothing left to do on your end.

mutation CreateBooking($request: CreateBookingInput!) {
  createBooking(input: $request) {
    __typename
    ... on Booking {
      id
    }
    ... on Error {
      message
    }
    ... on InvalidCustomerError {
      invalidFields
    }
  }
}

Request variables

{
  "request": {
    "bookingQuoteId": "<booking-quote-id>",
    "customer": {
      "name": "Product Demo",
      "email": "valid.email.address@peek.com",
      "phone": "0196488932",
      "country": "US",
      "postalCode": "94501"
    }
  }
}

Response

{
  "data": {
    "createBooking": {
      "__typename": "Booking",
      "id": "<booking-id>"
    }
  }
}

CreateBooking Errors

Invalid Customer

If you provide invalid customer details, you will receive the InvalidCustomerError with details about the invalid fields detected.

Request

{
  "request": {
    "bookingQuoteId": "<booking-quote-id>",
    "customer": {
      "name": "Product Demo",
      "email": "invalid_email_address",
      "phone": "0196488932",
      "country": "US",
      "postalCode": "94501"
    }
  }
}

Response

{
  "data": {
    "createBooking": {
      "__typename": "InvalidCustomerError",
      "invalidFields": [
        "email"
      ],
      "message": "Please provide valid customer details. Invalid: email."
    }
  }
}

Invalid Questions Error

If you forget to provide required questions answers to the quote you want to proceed and make a booking for, you will receive the InvalidQuestionsError with details about the invalid questions detected.

Request

{
  "request": {
    "bookingQuoteId": "<booking-quote-id>",
    "customer": {
      "name": "Product Demo",
      "email": "invalid_email_address",
      "phone": "0196488932",
      "country": "US",
      "postalCode": "94501"
    }
  }
}

Response

{
  "data": {
    "createBooking": {
      "__typename": "InvalidQuestionsError",
      "message": "Missing guest answers for tickets: <ticket-id>. Missing required questions: <question1-id>, <question2-id>, ..., <questionN-id>"
    }
  }
}

Source Error

Sometimes, when creating a booking, the activity provider may have any kind of error while processing the booking request. In this case you will receive an error like this one.

{
  "data": {
    "createBooking": {
      "__typename": "SourceError",
      "message": "Something unexpected happened, please retry your request. Engineers are notified and will report back."
    }
  }
}

Request variables

{
  "request": {
    "bookingQuoteId": "qb0nm9r",
    "customer": {
      "name": "Peek Plus Demo",
      "email": "name@example.com",
      "phone": "0196488932",
      "country": "US",
      "postalCode": "12345"
    }
  }
}

CancelBooking

This will cancel the booking in our system and notify the provider that the customer will no longer be coming. We are assuming a refund will be issued on your end and update the customer in our system to indicate a refund has been granted.

mutation CancelBooking($input: CancelBookingInput!) {
  cancelBooking(input: $input) {
    __typename
    ... on Booking {
      status
    }
    ... on Error {
      message
    }
  }
}

Request variables

{
  "input": {
    "bookingId": "<booking-id>",
    "cancellationReason": "WEATHER"
  }
}

Response

{
  "data": {
    "cancelBooking": {
      "__typename": "Booking",
      "status": "CANCELED"
    }
  }
}

See CancellationReason

CancelBooking Errors

CancellationRejectionError

If you do not provide a cancellationReason you will get this error.

Request variables

{
  "input": {
    "bookingId": "<booking-id>"
  }
}

Response

{
  "data": {
    "cancelBooking": {
      "__typename": "CancellationRejectionError",
      "message": "You must provide a valid cancellation reason when cancelling a booking."
    }
  }
}

Webhooks

Booking Cancellation

Typically, when you send a booking into PeekPlus and the customer needs to cancel, they will reach out to your company and your company will cancel the booking. This cancellation will make its way to the underlying supplier to let them know the customer will no longer be coming.

However, there are times when the supplier has to cancel the booking and they need to let YOU know that it happened so you can do whatever it is you need to on your end (refund the customer, update any records in your system, etc).

The first step is to reach out to your contact at PeekPlus and provide the url to hit (for both sandbox and production) for these webhooks (ex https://peekplus.example.com/webhooks).

Once that's been done, anytime we need to notify you of a cancellation, we will POST to [YOUR_URL_PROVIDED_ABOVE]/peek-plus-webhooks/bookings/[BOOKING_ID]/cancel.

For authentication we will send an auth header w/ a JWT that is signed w/ your api key + secret; just like you send to us for your requests.

To test this process, we added the ability to "Trigger" one of these async webhooks yourself:

mutation TriggerCancellationWebHook($bookingId: ID!) {
  triggerCancellationWebHook(bookingId: $bookingId) {
    message
  }
}

If you open your GQL Client, make a booking and then use the above mutation, it'll give you real-time feedback on what your server returned to help you with debugging.

Try It Out

Once you have been granted an API Key + API Secret click the button below.

This will get you an entire working api ready to click through.

Run in Insomnia}

Go Live Process

Updates

Date Description
Nov 6, 2023 Removed section: Mutations. Added new sections: Entities CreateBookingQuote, CreateBookingQuote Errors, UpdateBookingQuote, UpdateBookingQuote Errors, CreateBooking, CreateBooking Errors, CancelBooking, CancelBooking Errors and Updates
Jun 7, 2022 Added new sections: Go Live Process and Webhooks
May 13, 2020 Added new section: Try It Out
Jan 31, 2020 Initial Docs