Skip to content

Instantly share code, notes, and snippets.

@darrentorpey
Last active October 6, 2021 20:23
Show Gist options
  • Save darrentorpey/0e6946e5388a8ffab4b3cd29a413b40a to your computer and use it in GitHub Desktop.
Save darrentorpey/0e6946e5388a8ffab4b3cd29a413b40a to your computer and use it in GitHub Desktop.
API Design

PDP API Callouts example

Callouts today

Here's what callouts field looks like today:

{
  "callouts": [
    {
      "type": "rue30ShippingUpsell",
      "icon": "truck",
      "message": "Place an order today, then ship free for 30 days"
    },
    {
      "type": "rue30ManyDaysLeft",
      "icon": "truck",
      "message": "You have %d days left of FREE shipping"
    },
    {
      "type": "rue30LastDay",
      "icon": "truck",
      "message": "You have 1 day left of FREE shipping"
    },
    {
      "type": "rue365FreeShipping",
      "icon": "truck",
      "message": "Free Shipping is all yours"
    },
    {
      "type": "internationalExcluded",
      "icon": "truck",
      "message": "Ships to the U.S."
    },
    {
      "type": "domesticReturns",
      "icon": "arrow",
      "message": "This item has an extended holiday return period. All items purchased now through September 30 can be returned through January 15."
    },
    {
      "type": "internationalReturns",
      "icon": "arrow",
      "message": "Discounted shipping & returns are available in select countries"
    }
  ]
}

The client uses data about the member to determine which of these callouts to show:

It chooses one of these four based on the member's shipping program benefits and whether the member is shopping internationally:

  • rue30ShippingUpsell
  • rue30ManyDaysLeft
  • rue30LastDay
  • rue365FreeShipping
  • internationalExcluded

Client chooses one of these two based on whether the member is shopping internationally:

  • domesticReturns
  • internationalReturns

further, domesticReturns's message varies depending on whether the product is Final Sale and whether we're in the Extended Returns window.

Callouts future?

Instead of splitting business logic between the API and the client, we can have the API do all the thinking and have it simply tell the client which things to display and exactly what to display with them:

Ex 1

{
  "calloutBlocks": [
    {
      "name": "shippingMessage",
      "icon": "truck",
      "message": "Place an order today, then ship free for 30 days"
    },
    {
      "name": "returnsMessage",
      "icon": "arrow",
      "message": "This item has an extended holiday return period. All items purchased now through September 30 can be returned through January 15."
    },
  ]
}

Ex 2

{
  "calloutBlocks": [
    {
      "name": "shippingMessage",
      "icon": "truck",
      "message": "Ships to the U.S."
    },
    {
      "name": "returnsMessage",
      "icon": "arrow",
      "message": "Discounted shipping & returns are available in select countries"
    },
  ]
}

No matter how many things determine which messages to show and what content should be in each, the API alone is responsible for making the determination. The client needs only know which types to look for and for each type:

  • Where to render it on the screen
  • How to format the content (font, size, icons, bold font, brand coloring, etc.)

More speculation

We may want to roll our GFH messaging into the callouts or calloutBlocks (or whatever we call it) field, so we'd have something like:

Ex 3

{
  "calloutBlocks": [
    {
      "name": "shippingMessage",
      "icon": "truck",
      "message": "Ships to the U.S."
    },
    {
      "name": "gfhMessage",
      "icon": "gift",
      "message": "This item is guaranteed to ship in time for Christmas"
    },
    {
      "name": "returnsMessage",
      "icon": "arrow",
      "message": "Discounted shipping & returns are available in select countries"
    },
  ]
}

and when GFH is not applicable to the product, that gfhMessage block would simply not be there.

PDP API - Low inventory warning and sold out notice

LIW / SO today

Here's how the API currently instructs the client on when to show a Low Inventory Warning ("4 Left") and when to show "Sold out".

When a SKU is not selected, the client reads totalInventory and renders "Sold out" if it is 0. Otherwise, it reads showLowInventoryWarning and if it's true then it shows "% Left" where % is filled in with the value from totalInventory.

When a SKU is selected, the client reads inventory from that's SKUs block in the skus field and renders "Sold out" if it is 0. Otherwise, it reads showLowInventoryWarning and if it's true then it shows "% Left" where % is filled in with the value from inventory from that's SKUs block.

{
  "showLowInventoryWarning": false,
  "totalInventory": 13,
  "skus": [
    {
      "inventory": 0,
      "showLowInventoryWarning": false,
    },
    {
      "inventory": 5,
      "showLowInventoryWarning": true,
    },
    {
      "inventory": 5,
      "showLowInventoryWarning": true,
    }
  ]
}

LIW / SO future?

Example

{
  "lowInventoryMessage": "3 Left",
  "skus": [
    {
      "lowInventoryMessage": "Sold out",
    },
    {
      "lowInventoryMessage": "3 Left",
    },
  ]
}

Example

{
  "lowInventoryMessage": "5 Left",
  "skus": [
    {
      "lowInventoryMessage": "2 Left",
    },
    {
      "lowInventoryMessage": "3 Left",
    },
    {
      "lowInventoryMessage": "Sold out",
    },
  ]
}

Limit set to 10

{
  "skus": [
    {
      "lowInventoryMessage": "9 Left",
    },
    {
      "lowInventoryMessage": "9 Left",
    },
  ]
}

All SKUs sold out

{
  "lowInventoryMessage": "Sold out",
  "skus": [
    {
      "lowInventoryMessage": "Sold out",
    },
    {
      "lowInventoryMessage": "Sold out",
    },
  ]
}

Alternatives

Catch-all messages field, also embedded within SKUs:

{
  ...
  "messages": [
    {
      "name": "lowInventoryMessage",
      "content": "Sold out",
    },
  ],
  "skus": [
    {
      ...
      "messages": [
        {
          "name": "lowInventoryMessage",
          "content": "Sold out",
        },
      ],
    },
    {
      ...
      "messages": [
        {
            "name": "lowInventoryMessage",
            "content": "Sold out",
        },
      ],
    },
  ]
}

Loyalty messaging data in Offers API

What I think matters

  • We all understand why we're chosing the strategy we go with
  • We recognize that only experience will tell us what we truly like and don't like, hence why being solid on the first point is key to having good retro and learning going forward
  • Is one format or another easier or harder for iOS, Android, and/or web clients to handle? (I don't think it matters much for web)
  • Would one format or another make it easier or harder for us to adapt these API data sets to changes?
  • What would happen if we needed to replace 3 of these 9 (or so) fields with another set? Would grouping them (see some of the examples below) help or might it be a distraction or point of confusion? Would we likely just throw our hands up and say "nevermind, we'll just make a new top-level field name (loyaltyMessagesV2?) if/when we have to change it significantly?
  • Are there any other special / interesting types like links that we should be considering?

Some options to consider

Option A: Standard types within an array

{
  "loyaltyInformation": [
    {
      "type": "shippingExpenseLabel",
      "message": "***Rue 365*** Shipping Perk"
    },
    {
      "type": "shippingExpenseValue",
      "message": "FREE"
    },
    {
      "type": "checkoutCalloutPrimary",
      "message": "***Rue 365*** Free Shipping and Returns* are all yours"
    },
    {
      "type": "checkoutCalloutSecondary",
      "message": "* Applies to Standard Shipping only (not available to AK & HI)"
    },
    {
      "type": "loyaltyUpsellPrimary",
      "message": "Add ***5-Star Perks*** - FREE Standard Shipping, Exclusive Boutiques and more!"
    },
    {
      "type": "loyaltyUpsellSecondary",
      "message": "Just $30 for the first year*"
    },
    {
      "type": "loyaltyUpsellFinePrint",
      "message": "*$50 per year to renew; Unlimited standard shipping on eligible items; continues for consecutive one-year periods; cancel at any time."
    },
    {
      "type": "loyaltyUpsellLink",
      "message": "Learn More",
      "url": "https://..."
    }
  ]
}

Option B: Custom-shaped bespoke object

{
  "loyaltyInfo": {
    "shippingExpenseLabel": "***Rue 365*** Shipping Perk",
    "shippingExpenseValue": "FREE",
    "checkoutCalloutPrimary": "***Rue 365*** Free Shipping and Returns* are all yours",
    "checkoutCalloutSecondary": "* Applies to Standard Shipping only (not available to AK & HI)",
    "loyaltyUpsellPrimary": "Add ***5-Star Perks*** - FREE Standard Shipping, Exclusive Boutiques and more!",
    "loyaltyUpsellSecondary": "Just $30 for the first year*",
    "loyaltyUpsellFinePrint": "*$50 per year to renew; Unlimited standard shipping on eligible items; continues for consecutive one-year periods; cancel at any time.",
    // Embedded object for links?
    "loyaltyUpsellLink": {
        "text": "Learn More",
        "url": "https://..."
    }
  }
}

Option C: Custom-shaped nested objects, with common structures?

{
  "loyaltyInfo": {
    "shippingExpense": {
        "label": "***Rue 365*** Shipping Perk",
        "value": "FREE",
    },
    "checkoutCallout": {
        "primary": "***Rue 365*** Free Shipping and Returns* are all yours",
        "secondary": "* Applies to Standard Shipping only (not available to AK & HI)",
    },
    // Embedded object for grouped fields
    "loyaltyUpsell": {
        "primary": "Add ***5-Star Perks*** - FREE Standard Shipping, Exclusive Boutiques and more!",
        "secondary": "Just $30 for the first year*",
        "finePrint": "*$50 per year to renew; Unlimited standard shipping on eligible items; continues for consecutive one-year periods; cancel at any time.",
        // Embedded object for links?
        "loyaltyUpsellLink": {
            "text": "Learn More",
            "url": "https://..."
        }
    },
    // Embedded object for links, but at level 2 depth and never lower?
    "loyaltyUpsellLink": {
        "text": "Learn More",
        "url": "https://..."
    }
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment