Order Validation

Provider Enablement / Endpoints

Create Order Validation

For a given location, LevelUp will pass parameters that identify which menu items and options a user would like to order (using the identifiers returned from the “Show Menu” call) along with a desired ready time in the timezone of the location (ISO8601), a tip amount, user details (name, phone number, email), any stored metadata (which may include other login credentials if an account was created previously), and a discount amount (if applicable). We expect a response with the final amount, applicable taxes, the soonest time at which the order could be available (in the local timezone of the location and ISO8601), which will be presented to the user.

Request Endpoint

POST /locations/:provider_location_id/order_validations

Example Request Body

{
  "order_validation": {
    "desired_ready_time": "2016-03-11T11:44:08",
    "fulfillment_type": "pickup",
    "grubhub_order_number": "430515324805810",
    "items": [
      {
        "item": {
          "category_id": "11124",
          "category_group_id": "6734384",
          "name": "Turkey Sandwich",
          "price": 679,
          "provider_id": "1324",
          "quantity": 1,
          "recipient_name": "Joe Smith",
          "metadata": {
            "integration_id_needed_for_validation": "1234"
          },
          "special_instructions": "Hold the mayo!",
          "options": [
            {
              "option": {
                "metadata": {
                  "integration_id_needed_for_validation": "1111"
                },
                "price": 100,
                "option_group_display_order": 2,
                "provider_id": "67478",
                "option_group_id": "585",
                "quantity": 2
              }
            },
            {
              "option": {
                "price": 0,
                "option_group_display_order": 1,
                "provider_id": "32791",
                "option_group_id": "596",
                "quantity": 1
              }
            }
          ]
        }
      },
      {
        "item": {
          "category_group_id": "6734384",
          "category_id": "75759",
          "name": "Slice of cake",
          "price": 199,
          "provider_id": "9876",
          "metadata": {
            "integration_id_needed_for_validation": "5678"
          },
          "quantity": 2,
          "recipient_name": "Jane Smith",
          "options": [
            {
              "option": {
                "price": 0,
                "option_group_display_order": 2,
                "provider_id": "62459",
                "option_group_id": "334",
                "quantity": 1
              }
            }
          ]
        }
      }
    ],
    "location_time_zone": "America/New_York",
    "metadata": {},
    "paid_via_ach": true,
    "tip": 100,
    "merchant_funded_discount": 100,
    "special_instructions": "please double bag my order",
    "include_disposable_utensils": true,
    "user": {
      "born_at": "1986-03-11T11:44Z",
      "first_name": "joe",
      "gender": "male",
      "last_name": "smith",
      "email": "example@gmail.com",
      "password": "password123",
      "phone": "4445556666"
    }
  }
}

Example Response Body

{
  "order_validation": {
    "available_at": [
      "2016-03-25T15:20:00Z",
      "2016-03-25T15:40:00Z",
      "2016-03-25T16:00:00Z",
      "2016-03-25T16:20:00Z",
      "2016-03-25T16:40:00Z",
      "2016-03-25T17:00:00Z",
      "2016-03-25T17:20:00Z",
      "2016-03-25T17:40:00Z",
      "2016-03-25T18:00:00Z"
    ],
    "tax": 123,
    "total": 978,
    "tip": 100,
    "soonest_available_at": "2016-03-11T11:44",
    "provider_funded_discount": 200,
    "merchant_funded_discount": 100,
    "service_fee": 50,
    "metadata": {
      "authentication_token": "asdf232342453asdTSfs"
    }
  }
}

Example Request Body (Delivery)

{
  "order_validation": {
    "desired_ready_time": "2016-03-11T11:44:08",
    "fulfillment_type": "delivery",
    "grubhub_order_number": "443213323803422",
    "delivery_fee": 300,
    "delivery_address": {
      "street_address": "One Federal St",
      "extended_address": "Floor 6",
      "locality": "Boston",
      "region": "MA",
      "postal_code": "02110",
      "latitude": 42.356257,
      "longitude": -71.057225
    },
    "items": [
      {
        "item": {
          "category_id": "11124",
          "category_group_id": "6734384",
          "name": "Turkey Sandwich",
          "price": 679,
          "provider_id": "1324",
          "quantity": 1,
          "recipient_name": "Joe Smith",
          "metadata": {
            "integration_id_needed_for_validation": "1234"
          },
          "special_instructions": "extra mayo",
          "options": [
            {
              "option": {
               "metadata": {
                 "integration_id_needed_for_validation": "1111"
               },
                "price": 100,
                "option_group_display_order": 2,
                "provider_id": "67478",
                "option_group_id": "585",
                "quantity": 2
              }
            },
            {
              "option": {
                "price": 0,
                "option_group_display_order": 2,
                "provider_id": "32791",
                "option_group_id": "596",
                "quantity": 1
              }
            }
          ]
        }
      },
      {
        "item": {
          "category_group_id": "6734384",
          "category_id": "75759",
          "name": "Slice of cake",
          "price": 199,
          "provider_id": "9876",
          "metadata": {
            "integration_id_needed_for_validation": "5678"
          },
          "quantity": 2,
          "recipient_name": "Jane Smith",
          "options": [
            {
              "option": {
                "price": 0,
                "option_group_display_order": 5,
                "provider_id": "62459",
                "option_group_id": "334",
                "quantity": 1
              }
            }
          ]
        }
      }
    ],
    "location_time_zone": "America/New_York",
    "metadata": {},
    "paid_via_ach": false,
    "tip": 100,
    "merchant_funded_discount": 100,
    "special_instructions": "buzzer does not work, call phone number please",
    "include_disposable_utensils": false,
    "user": {
      "born_at": "1986-03-11T11:44Z",
      "first_name": "joe",
      "gender": "male",
      "last_name": "smith",
      "email": "joe.smith@wexample.com",
      "password": "password123",
      "phone": "4445556666"
    }
  }
}

Example Response Body (Delivery)

{
  "order_validation": {
    "available_at": [
      "2016-03-25T15:20:00Z",
      "2016-03-25T15:40:00Z",
      "2016-03-25T16:00:00Z",
      "2016-03-25T16:20:00Z",
      "2016-03-25T16:40:00Z",
      "2016-03-25T17:00:00Z",
      "2016-03-25T17:20:00Z",
      "2016-03-25T17:40:00Z",
      "2016-03-25T18:00:00Z"
    ],
    "delivery_fee": 300,
    "tax": 123,
    "total": 978,
    "tip": 100,
    "soonest_available_at": "2016-03-11T11:44",
    "provider_funded_discount": 200,
    "merchant_funded_discount": 100,
    "service_fee": 0,
    "metadata": {
      "authentication_token": "asdf232342453asdTSfs"
    }
  }
}

Payment Methods

There are two methods of LevelUp paying for an order.

  • LevelUp As Tender: In this method, LevelUp will pass in simply “LevelUp” as the tender method, and using our API key for access to your systems, you will demarcate the order as paid for via LevelUp. We will pay the merchant directly via ACH next day. This is the method that is invoked when paid_via_ach is set to “true”.

  • Single Use Credit Card: LevelUp can pay any merchant indirectly, by passing to you a single-use credit card that we generate in real-time, and then charge the consumers card on file out-of-band via our secure vault. To you, that single-use corporate card looks just like a normal user card, but in reality, it’s a unique real-time generated single use token (card) that we pass to you to mirror your existing customer credit card payment flow. This is the method used when paid_via_ach is set to “false”.

Amounts

All amounts are always in cents.

Handling Nested Options

Please note that LevelUp will return options in a “flattened” fashion in our OV/OS requests. So if you believe that a given ordering provider will require that options be presented in a nested fashion, you will need to encode that nesting logic into your menu response to use, demarcating option ids and their parent option groups using some delimiter such as “:” or “-”. I.E. parent_option_group:option_id so that you can parse it back out as needed.

Handling Time Values

There are three options for how you pass back the date-time:

  • Local Times (preferred): Pass back the datetimes as the time you get back from the provider (after confirming that that is the location’s timezone) and should be formatted YYYY-MM-DDTHH:mm. (e.g. 2017-07-31T12:15)

  • Local Times (with timezone): Pass back the datetimes as the time you get back from the provider including the location timezone and should be formatted YYYY-MM-DDTHH:mm:ss-location_timezone. (e.g. 2017-07-31T12:15:00-04:00 where 04:00 denotes EST/EDT)

  • Named Time Zone: Pass back the datetimes as the time you get back from the provider including the timezone, this is likely in UTC and thus will be appended with a Z and should be formatted YYYY-MM-DDTHH:mmZ. We’ll convert it to the local time on our end. (e.g. 2017-07-31T12:15Z)

  • Relative Time: If the provider only gives you back a “N minutes from now” pass that back to us as expected_ready_at + 30 minutes from your server’s timezone, and denote that timezone. This will often be very similar to the case above and should be formatted YYYY-MM-DDTHH:mmZ. (e.g. 2017-07-31T12:15Z)

Handling Tips

LevelUp should only be passing in tip values for places that accept tips, as identified on the locations endpoint. However, if LevelUp passes in a tip for a place that does not accept tips, please return the tip attribute to us as null. We’ll interpret that as 0 for the transaction in question, but it will also trigger us to change our settings for that location’s ability to accept tips.

Total, Tax, Tip and Merchant or Provider Funded Discounts

The math behind an order can be a little confusing, so here are how the terms tax, tip and total are to be defined. As a reminder, all values are always in cents.

  • Tax = the sales tax on the order

  • Tip = the tip the user submitted, i.e. what we sent in to you can mostly just be echoed back to us

-If the site doesn’t allow tips, or the tip fails, return null. This is either an integration issue or a config issue as whether or not a location accepts tips should be defined on the menu for the location.

  • Service_fee: Fee to the user applied by an ordering provider.

  • Delivery_fee: Fee to the user for Delivery orders.

  • Merchant_Funded_Discount: If the merchant is running a discount through the LevelUp Platform, we will post the total discount value here, so it can be used in tax calculations.

  • Provider_Funded_Discount: If you are offering a provider funded discount, please return that here.

  • Total = the amount spent on food and does not include the tax or tip and specifically excludes any provider_funded_discount or merchant_funded_discount

As an example, a $10 sandwich with a $2 tip and $0.70 sales tax, $0.45 service fee and a $1 merchantfundeddiscount would have the following values:

  • Tax = 70 cents

  • Tip = 200 cents

  • Merchant_Funded_Discount = 100 cents

  • Service_fee = 45 cents

  • Total = 1000 cents

The total the consumer would be charged (which is not expected to be returned in the API to us) would be $12.15.

The total that LevelUp would end up paying to the merchant would be $12.15 since the $1 merchant funded discount is funded by the merchant.

Utensils support

  • By default all Grubhub orders will have include_disposable_utensils set to true. When a diner opts out of utensils via the Marketplace UI, order payloads will reflect this by sending include_disposable_utensils as false. This is true for both Order Validation and Order Submission endpoints.

Ready Times & Scheduling of Orders

  • If the desired_ready_time is null, the order should be validated to be placed ASAP. Currently, desired_ready_time can be passed in any of the three timezone formats mentioned in Handling Time values.

-If the desired_ready_time is null, and the location is closed, please attempt to schedule the order for the soonest available ready_time and return that along with the array of available ready times after that. Please be sure to include the full timestamp (inclusive of days) as this case is likely to span days.

  • When a desired_ready_time is included in the validation request, the order should be validated with a pick up time scheduled to the desired_ready_time.
    • Will always be in the locations time zone
    • In the case when a merchant only allows order scheduling on specific timeslots, the order should be validated to be picked up at the soonest timeslot available after the desired_ready_time and that timeslot should be returned as the soonest_available_at.
    • If the merchant allows scheduling and does not require users to pick specific time slots, and a desired_ready_time is provided, the desired_ready_time should be returned as the soonest_available_at.

Note that when the order is placed to be picked up ASAP (desired_ready_time=null), we expect the soonest_available_at time to you as a parseable time-stamp in the local time of that location. That’s the most useful time for us to receive and what we’ll show to the user.

If the merchant doesn’t give an actual ready time that can be returned to us as a date-time, please include the ready_at attribute, but have it’s value be null. We only want the most accurate value if a real time is returned otherwise LevelUp will sub one in based on our best guesstimate. You returning null will be our key to do that. So, if the merchant just says “ASAP” return null. If they say “In 10 minutes” or some other bit of text you can’t parse, just return null.

Available At Times

For merchants that support order scheduling but require orders to be scheduled for particular time slots, an array of available_at times should be returned. Only available time slots after the desired ready time and within 24 hours of the validation time should be returned in the available_at array. If ordering is available for any time after the soonest_available_at time, an available_at array should be returned. The available_at array should be null for providers that do not support order scheduling. Available time zones should be returned either as local timestamps (YYYY-MM-DDTHH:mm) or timestamps with a timezone (YYYY-MM-DDTHH:mmZ).

Account Credentials

LevelUp prefers to checkout as a guest when possible. However, if you are creating a user account and returning account credentials that you’d like us to store, please name them as follows: user_[whatever you prefer]. So for example it could be user_username and user_password or user_name and user_auth_key. We simply use the fact that it’s prefaced with “user_” to know to store it in a persistent fashion. This is in comparison to data that should only be stored in the context of a single order (like cart_ids) which are prefixed with order_ and described in more detail below.

Add Ons

LevelUp has it’s own merchant-controlled logic for upsells, so there’s no need to return to us existing upsell logic unless specifically requested by the merchant.

Optional Cart_ID Optimization

If you would like to include a cart_id, or some other identifier that you get back after the validate order call. To make the submit order call faster, you can echo that information back to LevelUp in the metadata response attribute, and we will send it along on the proceeding submit call. For example, if you sent us back the following on the validate call:

{
  "metadata": {
    "order_user_token": "a694d3bc-151b-48b0-bb92-a557106b0019",
    "order_cart_token": "d1e19c7b-e63a-4fb4-8896-912d12e1d15d"
  }
}

You would get it back on the submit call and could use it to pull back up the cart without recreating it from scratch. This might save a lot of time for you on the submit call.

Some things to note if you take this approach:

  • We will always still create a brand new cart on the validate call

  • If the necessary information is not received on the submit order call, (say due a timeout if your order validation call didn’t respond in time) you will need to be ready to still create the cart from scratch on submit.

  • Please always prefix your order-specific metadata with “order_”. This will let us know to clear it out before sending you a new validate order request, but persist it for the following submit order request. Order_ scoped metadata is persisted only for the order while User_ metadata is persisted long-term.

There are several types of errors that could occur:

  • Site Errors: 422 HTTP status code

    • The underlying merchant ordering site renders an error that would have been displayed to the user to explain something like “Sorry, this location is closed right now” or “Oh no! We’re out of guacamole”. In this case, please return the actual error message the site renders to the user, an error code of 422.
  • Item Or Option Unavailable Errors: 422 HTTP status code

    • This is a subset of the case above, but should be handled with the following criteria in mind:
    • Please attempt to return an error message to us with the item/option name to us, rather than the ID, so that the user can amend their order appropriately. We will pass this message along to the user.
    • Please also include extra attributes of failed_items and/or failed_options with an array of the item_ids and/or option_ids that are problematic.
    • Problematic item_ids or option_ids might be items or options that are:
      • Not available at this time
      • No longer on the menu
      • Out of stock at the moment
      • Simply unrecognized from your perspective
  • Integration Errors: 500 HTTP status code

    • If your service cannot interact with the merchant for some reason, please pass back a relevant message to help us understand what’s going wrong along with an error code 500. LevelUp will display its own error message to the user.
    • This should include the scenario when the underlying merchant is non-responsive, i.e. perhaps their location ordering provider is down, in which case your integration let LevelUp know so we can communicate to the user.
  • Parameter Errors: 500 HTTP status code

    • If we send you parameters that do not match your expectations and a message telling us the error. This error will not be shown to the user, but may be used for informing us we need to refresh our menu for the location. Send an error code of 500.
  • Menu Not found: 404 Error Code

    • If LevelUp attempts to issue a call and the merchant/location is no longer available from the provider (whether temporarily or permanently) subsequent requests from LevelUp for actions on that merchant should return a 404.
  • Service Down

    • If your service is down, LevelUp will return a generic error to the user. Error code 500.
  • Timeout Threshold

    • Order validation request expects a response within 25 seconds.