Skip to content

Concepts

General Documentation Information

Data Types

The api is written in C#, and as a result the data types in the documentation refer to C# data types. These map cleanly to similar types in other languages and generally do not need expanded upon. The only exception is the ? operator. This indicates a value may be null. So for example, if a data type is specified as bool? that indicates that the value may be null, and if it is not, then the type will be a bool. This also indicates that the api will accept a null in this field.

API Access Keys

The Vendor API uses AWS api keys to control access to the api and to manage rate limiting. If you do not have a token, email us!. To use your key all your requests MUST include the header x-api-key: your-aws-api-key. We have a route to help you test that this is setup properly, see the API key test route for more information.

If you do not have an API key, perform the following:

  1. Create one or more User(s) within the Organization that requires API access. a. We suggest api@organizationName.com for the login email. b. Set a strong password - the password for this user will be used in conjunction with an organization specific API key to authenticate against the API.
  2. Contact api-request@vendnovation.com and provide your Organization Name and the login email(s) of those users you wish to enable API access for.
  3. VendNovation will enable API access for those Users and send you your Organization specific API Key.

Usage Plan Details

All new API access keys are limited to the following restrictions:

Limits
Rate 20 requests per second
Burst 100 requests
Quota 5,000 requests per day

Proper call optimization should allow you to stay within the limits. Remember that all calls support retrieving multiple data sets by ID simultaneously.

API Stages

The Vendor API has multiple deployments you can access. It is recommended you use the most current version of the API. The stages available currently are:

Stage Description
rc An early access deployment of the next release. Code deployed to this stage can be expected to work. If the release contains breaking changes, those changes will generally be posted here one month before full deployment. Non-breaking changes may be posted closer to the full deployment date.
v1 The original production release of the api.
v2 A large rewrite featuring standardized request bodies and an enhanced feature set. Highlights are templating and role/rights support.
v2.1 A large non-breaking enhancement of v2. Additions include session management and support for uploading files and linking them to various records.

It is our goal to have the APIs exist in perpetuity, even when they are no longer receiving bugfixes. However, should a security-related issue be discovered that cannot be fixed in a non-breaking way, you will be given as much time as possible to migrate to a newer, secure version of the API.

Data Retrieval and Filters

{
  "requestFilter": {
    "organizationIds": [1],
    "siteIds": [1, 4, 6]
  }
}

The Vendor API is not structured like a standard REST API (parent-resource/{parent-resource-id}/child-resource) and is instead built around bulk operations and achieving as much as possible in a single request while still having resource driven routes. To support this goal, api urls are flat. Even though accounts are a logical child resource of organizations, they are accessed at <base-url>/accounts. As the change in URL structure would imply, you are able to retrieve accounts from multiple parent organizations at the same time. The default behavior of the API is to send you all the data about the specified resource type that you have access to. So if I have access to two organizations, and I call GET /machines, the api will return a result set of every single machine in both organizations at once.

As this is often too broad for a given use case, the API is filter driven. Almost every request accepts the standard request filter (found here) and supports multiple filters at the same time. In addition to the standard request filter, some route specific filters are also available and links to them will be found in the routes where applicable. Returning to our machines example, It could be narrowed down to only just the machines at any of the specified sites that also belong to a specific organization (see the example to the right). To get a single (or a few specific machines) you would only specify machineIdentifiers. These principles apply to all routes in the API.

It is worth noting that not only retrieval is a bulk action. Every call in the api supports bulk actions across multiple tiers of parent resources. Most bulk actions are treated as atomic sets, so if one element in your action fails, the entire set will be rolled back to its initial state, and you will be notified of such, with as helpful an error message as can be provided, unless a route's documentation states otherwise.

DateTime Handling

Timestamp fields are not universally UTC. The field name itself tells you which time zone a given timestamp is in. There are three patterns you'll see across the API:

Field name pattern Zone Example
Ends in Utc UTC startedAtUtc, recordedAtUtc, createdAtUtc
Ends in LocalTime The machine's local time zone occurredAtLocalTime
No zone suffix, paired with a timeZone field The machine's local time zone (see the accompanying timeZone field for which one) raisedAt / clearedAt (Alerts)

A Note on Time Zones

"Machine-local" means the time zone configured on the machine's site. Whenever a response includes a machine-local timestamp, it also includes a timeZone field alongside it, so you always know how to interpret the value without having to guess or look it up separately.

Two things worth knowing when working with dates and times:

  • A date-range filter compares in the time zone of the field it filters. If a route filters on a machine-local field, your range is applied in that machine's local time; if it filters on a ...Utc field, your range is applied in UTC. Check the individual route's documentation to see which field its date filter applies to.
  • A small number of fields predate this naming convention and don't match their name. For example, provisionedAt has no zone suffix but is actually always UTC. We can't rename a field that's already shipped without breaking existing integrations, so where a field's name and its actual behavior differ, that field's own documentation is the source of truth.

If you'd rather avoid time zone conversion on your end entirely, you can configure your sites to use UTC, and machine-local fields would then already be in UTC.

StartDate and EndDate Clamping

When you provide a StartDate and/or EndDate in your request filter, the dates you provided will not necessarily be the dates used in your request. We apply a series of clamping rules to your DateTimes to restrict your search range as well as generate either or both DateTimes if they were not both provided.

Two limits govern this, and they are not always the same number:

  • The maximum range is the widest span you may ask for. Ask for more and we narrow it.
  • The default range is the span we use when we have to invent a date you did not supply.

On most routes these are one and the same. Where a route can serve a wide window cheaply but most callers only want recent data, the two are separated so that raising what you may request does not change what you receive when you ask for no dates at all. Both values vary from route to route, so refer to the individual route documentation.

Here is a breakdown of this to help you understand how this works

StartDate Provided EndDate Provided Result
Yes Yes Your end date will be modified if the time span between provided start and end is larger than the maximum range
Yes No Your end date will be set to the default range number of days from your start date
No Yes The start date will be generated as your end date minus the default range
No No End date will be generated as DateTime.Now, and your start date will be the default range days earlier

So supplying one bound is not a way to reach the maximum range: only supplying both is. If you want a wide window, send both dates.

When applicable, the API response will include additional headers to help you detect when we have modified date-time search parameters. The header name follows the name of the range being filtered, so a route filtering a search range returns search_range_* while a route filtering raised and cleared ranges returns a pair for each.

  • <range>_range_start_date - will contain the modified search start date and time, if applicable.
  • <range>_range_end_date - will contain the modified search end date and time, if applicable.

These headers are how you discover the window we actually used. They are returned whether or not we changed anything, so comparing them against what you sent tells you if a limit applied.

Also of note, the time portion of the DateTime is optional. If omitted, StartDate will be given a time of 00:00:00 and EndDate will be given a time of 23:59:59.

Pagination

Paging is universally supported for get requests in the API. Page parameters are specified by headers.

Header Default Value Meaning
page 1 Which page of results are you requesting.
size 50 How many results per page would you like to retrieve. Will be clamped to the route's maximum page size if set higher. See individual route documentation for the specific limit.

The response's headers will include the paging information we used while processing the request as well as the total number of pages available to be retrieved.

Header Name Meaning
page The page of data returned by the API.
size The page size the api used while processing your request
total-pages How many pages of data are available to be retrieved.

Standardized Request Body

{
    "requestFilter": {
      // Request filter values, if any, go here.
    },
    "routeFilter": {
      // Route filter values, if any, go here.
    },
    "body": {
        //Your data, if any, goes here. For updates and creates this is an array of objects, not a single object
    }
}

Requests made to the api have standardized JSON keys to ensure uniformity of interaction with the API.

  • The request filter is to be assigned to a key named requestFilter.
  • Any route specific filter is to be assigned to a key named routeFilter.
  • Any data models you provide to the api (eg for creates or updates) is to be assigned to a key named body.

To simplify making requests, you can put this in every request body (sans comments) and add data on an as needed basis.

Request Time Budget

Every request runs under a per-request time budget of approximately 25 seconds, not 30. If a request has not returned within its budget, the API cancels the in-flight work, rolls back any pending write, and returns 408 REQUEST_TIMEOUT with the exact budget in the message body. The next request starts fresh; the timeout is scoped to one call, not to your token or your account.

The budget is shorter than the connection-level timeout you might see from your HTTP client because the API needs a small margin at the end of the call to cancel cleanly, roll back a write in progress, and serialize the error response. Setting your client timeout to something above 30 seconds does not extend it. If you need the full 30 seconds of wall-clock, split the request.

A slow request is almost always a wide filter, a long date range, or a request for a resource with many rows in your organization. When you see a 408:

  • Narrow the filter to a specific machineId, siteId or set of organizationIds.
  • Shorten the date range. A one-day window is typically well under budget for even the busiest resources.
  • Reduce the size header on a paginated read.
  • Retry: the timeout is deterministic against the request shape, so a retry with the same body will fail the same way.

A 408 is never a partial success. No data has been written and no partial result has been returned; the response body is the error only. It is also distinct from a 500 VN_INTERNAL_ERROR, which means the API itself had an unexpected failure. If you cannot narrow the request further and a 408 persists on a shape that used to work, that is worth flagging to VendNovation support.

Error Handling

{
  "code": "DATA_ACCESS_VIOLATION", // programmatically useful error code
  "message": "You tried to update one or more sites outside of your organization. No changes have been made.", // human friendly error message
  "pointers": [1, 4, 7], // an identifier used to find the problem records in your payload
  "pointerName": "Site.Id" // a string that tells you what field the pointers represent
}

When the api encounters an error, it will send back a list of error objects like the one seen in the sidebar. These objects should make it easy to programmatically detect and adjust for errors. Below is a table documenting these common error codes. If a route can have other errors not found in this table a similar table will be found in that route's documentation.

HTTP Code Error Code Definition
401 API_TOKEN Your api token was not present or has expired. Call /authenticate to get one.
403 NO_API_ACCESS You do not have api access. Please contact vendnovation.
500 ACCESS_ERROR Something went wrong while trying to retrieve your data access permissions.
403 DATA_ACCESS_VIOLATION An operation was requested for resources outside of your organizations.
500 VN_INTERNAL_ERROR Something went wrong on our end. Reach out to vendnovation support.
400 GENERIC_TRANSACTION_ERROR A database transaction failed while processing your request. No changes have been made.
400 UNEXPECTED_RESULT_COUNT Fewer rows were affected by your request than expected. No changes have been made.
404 MISSING_RECORD One or more of the specified records could not be found.
422 VALIDATION_ERROR One or more fields failed validation. See the error details for which fields and why.
422 DATA_VALIDATION_ERROR One or more fields failed business-logic validation. See the error details for more information.
422 MISSING_REQUIRED_DATA Your request was serializable but was missing some data required by the business logic. Please refer to the documentation.
422 DESERIALIZATION_ERROR One or more fields could not be deserialized to the expected type, or your request body was malformed.
400 INVALID_DATE_RANGE Your date range's start date was after its end date.
408 REQUEST_TIMEOUT Your request took longer than the per-request time budget. Retry with a narrower filter or a shorter date range.