Skip to content
On this page

REST API Walkthrough

The following example walkthrough will set up dynamic pricing for an imaginary art (painting) workshop. This walkthrough does not include all the features of the DynamO Pricing API, for more information visit Concepts or the API documentation.

Prerequisites

A project identifier is required for all API calls ({projectId} from now on, or ${projectId} in code sippets), and as well as a valid authentication token ({authToken}), which can be acquired by authenticating according to the API documentation. The token must be supplied in the Authorization HTTP header in the form of Bearer {authToken}.

Acquiring Authentication Tokens

In the test environment authentication tokens are longer lived, however in the production environment tokens are issued more frequently. Make sure to automate this step in your production workflows.

Acquiring a token with a service account:

ts
const response = await fetch({
  url: `https://auth.dynamopricing.com/oauth2/token`
  method: "POST",
  headers: {
    "Content-Type": "application/x-www-form-urlencoded",
  },
  body: "grant_type=client_credentials&client_id=serviceaccount-example-main&client_secret=secureSecret",
})
.then(res => res.json());

// Use this token in the API calls.
const authToken = response.token;

// Make sure to get new tokens before they expire.
const expiresInSeconds = response.token.expires_in;

Creating a New Pricing Instance

New instances will always be empty by default:

ts
const response = await fetch({
  url: `https://api.dynamopricing.com/projects/${projectId}/instances`
  method: "POST",
  headers: {
    "Authorization": `Bearer ${authToken}`
  }
})
.then(res => res.json());

// The instance can be referred to by this unique identifier from now on.
const instanceId = response.instanceId;

Configuring a Pricing Instance

Most interactions with pricing instances happen via Entries. The following example configures the instance in a single HTTP request, but it is not required, each entry could be provided separately.

This time the following configuration is provided, with additional comments in the code snippet:

  • instance metadata
    • name of the event
    • external identifier
    • location
    • third party information
    • instance dates, and automatic instance deactivation date
  • a product
    • the metadata of the product
      • name
      • external identifier
    • properties of the product
      • capacity
      • expected sales
  • items that are associated with the product
    • item metadata
      • name of the items
    • price
  • pricing schedule
ts
await fetch({
  url: `https://api.dynamopricing.com/projects/${projectId}/instances/${instanceId}/entries`
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": `Bearer ${authToken}`
  },
  body: JSON.stringify({
    // Optional, used to prevent duplicate requests.
    "requestId": "2d41c006-4af9-4459-bc5f-ae9c845bee00",
    // The entries in a single request are sorted by their timestamps.
    // The sorting algorithm used is stable, so entries with identical timestamps
    // will be applied in order of their definition.
    "entries": [
      {
        "entryType": "instanceMetadata",
        "timestamp": "1970-01-01T00:00:00+00:00",
        "meta": {
          // Metadata can be either replaced entirely or updated field-by-field.
          // In most cases a complete replacement will be required.
          "replace": {
            "externalId": "art_org__event_01",
            "event": {
              "name": "Art Workshop",
              "eventType": "other"
            },
            "location": {
              "id": "location_001",
              "name": "Art Organization Main Building"
            },
            "dates": [
              {
                "description": "End of pricing, workshop begins",
                "date": "1970-01-01T00:00:00+00:00",
                "actions": ["deactivateInstance"]
              }
            ],
            "thirdParty": {
              // In some cases third-party information is useful or even required for DynamO Pricing.
              "identifier": "11232134",
              "name": "Art Organization"
            }
          }
        }
      },
      {
        "entryType": "productRegistration",
        "timestamp": "1970-01-01T00:00:00+00:00",
        "productIds": ["workshop_seat"],
      },
      {
        "entryType": "productMetadata",
        "timestamp": "1970-01-01T00:00:00+00:00",
        "products": ["workshop_seat"],
        "meta": {
          "replace": {
            "externalId": "art_org__event_01__product_01",
            "name": "Workshop Seats"
          }
        }
      },
      {
        "entryType": "productProperties",
        "timestamp": "1970-01-01T00:00:00+00:00",
        "products": ["workshop_seat"],
        "properties": {
          "replace": {
            // We have 5 seats in the workshop.
            "capacity": 5,
            // We expect at least 4 seats to be sold.
            "expectedSales": {
              "count": 4
            }
          }
        }
      },
      {
        // We register each seat item individually.
        "entryType": "itemRegistration",
        "timestamp": "1970-01-01T00:00:00+00:00",
        "itemIds": [
          "workshop_seat1",
          "workshop_seat2",
          "workshop_seat3",
          "workshop_seat4",
          "workshop_seat5"
        ]
      },
      {
        // We give each item a name, it's not really important
        // in this example.
        "entryType": "itemMetadata",
        "timestamp": "1970-01-01T00:00:00+00:00",
        "items": [
          "workshop_seat1",
          "workshop_seat2",
          "workshop_seat3",
          "workshop_seat4",
          "workshop_seat5"
        ],
        "meta": {
          "replace": {
            "name": "Seat"
          }
        }
      },
      {
        "entryType": "itemProperties",
        "timestamp": "1970-01-01T00:00:00+00:00",
        "items": [
          "workshop_seat1",
          "workshop_seat2",
          "workshop_seat3",
          "workshop_seat4",
          "workshop_seat5"
        ],
        "properties": {
          "replace": {
            // In most cases each item must have start and end dates,
            // otherwise the pricing engine will not know when they should be sold.
            //
            // The item-specific dates are not necessarily related to the dates set in the
            // instance metadata.
            "dates": {
              "start": "1970-01-01T00:00:00+00:00",
              "end": "1970-01-01T00:00:00+00:00"
            },
            // Specific commissions for DynamO Pricing can be set for each item, do not
            // set these without notifying us beforehand.
            "commissionRate": {
              "multiplier": 0.05
            },
            "products": [
              {
                // All items are associated with the single product.
                "productId": "workshop_seat",
                // Limit the amount of items that can be sold.
                //
                // In this case it is not even required, since there will be only one
                // piece available of each item.
                "capacity": 1
              }
            ]
          }
        }
      },
      {
        // For seat #1-3 we set the same price range.
        "entryType": "itemPrice",
        "timestamp": "1970-01-01T00:00:00+00:00",
        "items": [
          "workshop_seat1",
          "workshop_seat2",
          "workshop_seat3"
        ],
        "price": {
          "allowed": {
            "min": 100,
            "max": 500
          },
          "default": 250
        }
      },
      {
        // Seat #4 is in a very good position, so we
        // set it to be a bit more pricier.
        "entryType": "itemPrice",
        "timestamp": "1970-01-01T00:00:00+00:00",
        "items": [
          "workshop_seat4"
        ],
        "price": {
          "allowed": {
            "min": 300,
            "max": 800
          },
          "default": 400
        }
      },
      {
        // Seat #5 however is in a far corner of the room,
        // so we only try to sell it at half price of seat #1.
        //
        // Selling seat #1 is not an issue since
        // the prices of the items are based on the product.
        "entryType": "itemPrice",
        "timestamp": "1970-01-01T00:00:00+00:00",
        "items": [
          "workshop_seat5"
        ],
        "price": {
          "match": {
            "itemId": "workshop_seat1",
            "multiplier": 0.5
          }
        }
      },
      {
        "entryType": "pricingSchedule",
        "timestamp": "1970-01-01T00:00:00+00:00",
        "schedule": {
          // The prices will be automatically calculated each hour
          // unless specifically requested via the API.
          "intervalSeconds": 3600
        }
      }
    ]
  }),
});

Getting Notified of New Prices

The prices of an instance is available via an API endpoint, however in most scenarios polling can be inefficient. It is possible to subscribe to price changes via the API.

New prices are broadcasted to each subscription individually, subscriptions are also stateful, so consequent prices will not be sent unless the prices change.

Let's create a subscription for all new prices within a project for all instances. Project subscriptions aggregate price changes and send them periodically.

ts
const response = await fetch({
  url: `https://api.dynamopricing.com/projects/${projectId}/subscriptions`
  method: "POST",
  headers: {
    "Content-type": "application/json",
    "Authorization": `Bearer ${authToken}`
  },
  body: JSON.stringify({
    "callbackUrl": "https://my-ticket-org.com/dynamo-pricing",
    "event": {
      "eventType": "projectPriceUpdate"
    },
    "token": "aVerySecureToken"
  })
})
.then(res => res.json());

// We can use the following ID to remove the subscription in the future.
const subscriptionId = response.subscriptionId;

It is also essential to subscribe for messages and alerts, so that notifications can be sent if any issues occur during pricing.

ts
const response = await fetch({
  url: `https://api.dynamopricing.com/projects/${projectId}/subscriptions`
  method: "POST",
  headers: {
    "Content-type": "application/json",
    "Authorization": `Bearer ${authToken}`
  },
  body: JSON.stringify({
    "callbackUrl": "https://my-ticket-org.com/dynamo-pricing",
    "event": {
      "eventType": "projectMessage"
    },
    "token": "aVerySecureToken"
  })
})
.then(res => res.json());

// We can use the following ID to remove the subscription in the future.
const subscriptionId = response.subscriptionId;

Then the callbacks can be handled whenever they are sent by DynamO Pricing:

ts
// In an express-like server:
app.post('/dynamo-pricing', async function(req, res) {
  const request = await req.json();

  // Always validate signatures,
  // refer to the webhook documentation for more information.
  if (!validateSignature(request)) {
    await res.status(401).send();
    return;
  }

  if (request.event.eventType === "projectPriceUpdate") {
    for (const { instanceId, prices } of request.event.instances) {
      await updatePrice(instanceId, prices);
    }
  } else if (request.eventType === "projectMessage") {
    // Most likely something bad happened.
    await sendAlert();
  }

  // Make sure to reply with a positive status,
  // otherwise the callback will be retried.
  await res.status(204).send();
});

Pricing Instance Activation

Pricing Instances are inactive by default, no prices are calculated and no callbacks are sent. It is possible to submit entries however.

In order to activate an instance a simple request is required:

ts
const response = await fetch({
  url: `https://api.dynamopricing.com/projects/${projectId}/instances/${instanceId}`
  method: "PUT",
  headers: {
    "Content-type": "application/json",
    "Authorization": `Bearer ${authToken}`
  },
  body: JSON.stringify({
    "active": true
  })
})
.then(res => res.json());

The instance can be deactivated and reactivated later any number of times in the same fashion.

Sales and Reservations

Item sales and reservations are important for automatic pricing to work. They can also be submitted via entries.

ts
const response = await fetch({
  url: `https://api.dynamopricing.com/projects/${projectId}/instances/${instanceId}/entries`
  method: "POST",
  headers: {
    "Content-type": "application/json",
    "Authorization": `Bearer ${authToken}`
  },
  body: JSON.stringify({
    "entries": [
      {
        "entryType": "itemSale",
        "timestamp": "1970-01-01T00:00:00+00:00",
        "items": [
          "workshop_seat1"
        ],
        "count": 1,
        "price": 400,
        // Each associated product that is affected by the sale must be listed.
        "products": [
          {
            "productId": "workshop_seat"
          }
        ]
      },
      {
        // Seat #4 was sold to a friend of the organization and the price
        // was the result of an agreement.
        //
        // These kind of sales do not count towards the commissions for
        // DynamO Pricing, however they still convey important information.
        "entryType": "itemSale",
        "timestamp": "1970-01-01T00:00:00+00:00",
        "items": [
          "workshop_seat4"
        ],
        "count": 1,
        "pricing": "external",
        "price": 5000,
        "products": [
          {
            "productId": "workshop_seat"
          }
        ]
      }
    ]
  })
});

The opposite of itemSale entries are itemReturns, however keep in mind that item returns will not affect the availability of items, while sales will.

Reservations can be sent in a similar fashion:

ts
const response = await fetch({
  url: `https://api.dynamopricing.com/projects/${projectId}/instances/${instanceId}/entries`
  method: "POST",
  headers: {
    "Content-type": "application/json",
    "Authorization": `Bearer ${authToken}`
  },
  body: JSON.stringify({
    "entries": [
      {
        "entryType": "itemReservation",
        "timestamp": "1970-01-01T00:00:00+00:00",
        "items": [
          "workshop_seat5"
        ],
        "count": 1,
        "products": [
          {
            "productId": "workshop_seat"
          }
        ]
      },
      // Item reservations have no opposite entries, in this case
      // a negative amount can be used to undo the reservation.
      {
        "entryType": "itemReservation",
        "timestamp": "1970-01-01T00:00:00+00:00",
        "items": [
          "workshop_seat5"
        ],
        "count": -1,
        "products": [
          {
            "productId": "workshop_seat"
          }
        ]
      }
    ]
  })
});