Yeti Public API (1.0.0-alpha)

Download OpenAPI specification:

The versioned, resource-oriented public API that customers and partners (starting with Zapier) use to import and export their Yeti data.

This is the contract, and it is the source of truth: CI fails if a route exists in the service without appearing here. It replaces the legacy public_access surface, whose as-is reference and redesign proposal live in docs/legacy-endpoints/ and docs/redesign-proposal.md in this repository.

Status

Phase 0 (YET-104). The cross-cutting contract below — authentication, scopes, errors, pagination, rate limiting — is settled and implemented. The resource endpoints (sites, catalog, parties, history, reports) arrive in Phase 1.

Authentication

Send a per-company API key as Authorization: Bearer <key>. Keys are issued by a company admin in the Yeti office UI, shown once, and stored only as a hash. Each key carries scopes of the form {resource}:{ability} — see GET /v1/reference/scopes.

Conventions

  • Errors are RFC 9457 application/problem+json documents. Switch on the code member, which is stable; type is a URI to documentation.
  • Collections return { data, meta, links } and page by cursor (?cursor=…&limit=…). There is no total: counting every match on every page is what makes large exports slow, and meta.page.has_more answers the question a paging client actually has.
  • Money is { "amount": "12.50", "currency": "USD" } — a decimal string so no client loses precision, and always an explicit ISO currency.
  • Timestamps are RFC 3339 in UTC.
  • Ids are strings, even where the underlying column is an integer.
  • Rate limiting is per API key, reported with RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset on every response.

Platform

Service health, credential introspection and reference data.

Service health

Liveness/readiness for the service and its hard dependencies. Requires no credential — a load balancer has none — and therefore discloses nothing beyond whether each dependency answers.

Responses

Response samples

Content type
application/json
{
  • "status": "ok",
  • "checks": {
    }
}

Describe the calling API key

What this key is, which company it is bound to, and which scopes it holds. The first call to make when setting up an integration: it turns "some endpoint returns 403" into "this key is missing that scope".

Never returns the key itself — only prefix…last_four.

Authorizations:
bearerAuth

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Identify the running build

Which image is serving this request — the commit, branch, tag and when it was deployed. Stamped into the image at build time, so it describes the running code rather than whatever the deployment configuration claims.

Unauthenticated, like /health, so anyone debugging a deployment can answer "what is actually out there?" without first being issued a key — which is the whole point of the endpoint. Nothing returned is a secret: a commit SHA is not a credential and is inert without the (private) repository.

Every field is nullable: an image built outside the deploy script carries no stamp, and the null is the answer — "this was not built by the deploy script" — rather than an error.

Responses

Response samples

Content type
application/json
{
  • "commit_hash": "4b1ee3bd2c9f7a05e18d6c3b2a91f4e7d0c85a63",
  • "branch": "main",
  • "tag": "untagged",
  • "deployed_at": "Fri August 15, 2026 4:12pm MDT"
}

List every grantable scope

The full scope vocabulary, as data rather than prose, so a client or an admin UI can enumerate it without hardcoding.

Unpaginated: the list is bounded by the resource catalogue.

Authorizations:
bearerAuth

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    }
}

Sites

Serviced locations. The resource most integrations are about, and the first collection endpoint — so it is also where the shared collection conventions (cursor paging, filter, sort, include, fields) are documented.

List sites

The calling company's sites, newest ids last, paged by cursor.

Exporting everything

Request the first page, then follow links.next until it is null. Do not rebuild the query string yourself — next already carries your filters, and the cursor is only valid alongside them.

The default order is id ascending. That is deliberate for exports: an immutable unique key is a total order, so a row edited while you page cannot move and be seen twice.

Exporting only what changed

filter[updated_since] is the incremental sync: store the time your last successful run started, and pass it next time.

GET /v1/sites?filter[updated_since]=2026-08-01T00:00:00Z

Scope

Requires sites:read.

Authorizations:
bearerAuth
query Parameters
cursor
string

An opaque cursor from links.next or links.prev. Do not construct or modify one: it encodes the sort values of the last row of the previous page and is only meaningful with the same filters and sort.

limit
integer [ 1 .. 200 ]
Default: 50

Rows per page. Clamped to the maximum rather than rejected.

sort
string
Example: sort=-updated_at,name

Comma-separated fields, - for descending: sort=-updated_at,name. At most 3. An unsortable field is a 422 listing what is sortable.

The primary key is always appended, so the order is total and paging cannot skip or repeat a row when values tie.

include
string
Example: include=contacts

Comma-separated related resources to embed, at most 5. A relation that is not included is absent from the response rather than null, so "not requested" is distinguishable from "requested, and empty".

object
Example: fields[sites]=name,address

fields[type]=a,b narrows a representation to the fields named.

id is always returned, and anything named in include is kept whether or not it also appears here — an include that produced nothing while the response was a 200 is the most expensive kind of silence.

object
Examples:
  • filter[city]=Calgary - Sites in one city
  • filter[updated_since]=2026-08-01T00:00:00Z - Incremental export
  • filter[area]=[object Object] - Larger than 5000 m²

filter[field]=value, or filter[field][operator]=value where the operator is one of eq, ne, in, gt, gte, lt, lte, contains, null. Which operators a field accepts depends on its type; an unaccepted one is a 422 that lists the alternatives.

Filterable: name, site_reference, client_reference, client_company_id, city, region, country, postal_code, area, created_at, updated_at.

Two named filters: filter[search] (free text over name, operator name, both references and city) and filter[updated_since].

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    },
  • "links": {}
}

Fetch one site

A single site of the calling company.

include and fields behave exactly as they do on the collection.

A site belonging to another company returns 404, not 403: saying "forbidden" would confirm the id exists.

Requires sites:read.

Authorizations:
bearerAuth
path Parameters
siteId
required
string

The site's id. A string on the wire, as all ids are.

query Parameters
include
string
Example: include=contacts

Comma-separated related resources to embed, at most 5. A relation that is not included is absent from the response rather than null, so "not requested" is distinguishable from "requested, and empty".

object
Example: fields[sites]=name,address

fields[type]=a,b narrows a representation to the fields named.

id is always returned, and anything named in include is kept whether or not it also appears here — an include that produced nothing while the response was a 200 is the most expensive kind of silence.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Catalog

What a company prices work from: services, equipment, equipment categories and consumables. Each family carries its own scope.

List services

The services this company sells. client is what it charges; subcontractor is what it pays.

Paged by cursor, id ascending — follow links.next until it is null. filter[updated_since] is the incremental export.

Requires services:read.

Authorizations:
bearerAuth
query Parameters
cursor
string

An opaque cursor from links.next or links.prev. Do not construct or modify one: it encodes the sort values of the last row of the previous page and is only meaningful with the same filters and sort.

limit
integer [ 1 .. 200 ]
Default: 50

Rows per page. Clamped to the maximum rather than rejected.

sort
string
Example: sort=-updated_at,name

Comma-separated fields, - for descending: sort=-updated_at,name. At most 3. An unsortable field is a 422 listing what is sortable.

The primary key is always appended, so the order is total and paging cannot skip or repeat a row when values tie.

object
Example: fields[sites]=name,address

fields[type]=a,b narrows a representation to the fields named.

id is always returned, and anything named in include is kept whether or not it also appears here — an include that produced nothing while the response was a 200 is the most expensive kind of silence.

object

Filterable: name, client_charge_type, subcontractor_charge_type, created_at, updated_at, plus filter[search] and filter[updated_since].

Rates are not filterable — see the Money schema for why.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    },
  • "links": {}
}

Fetch one record

A single record of the calling company.

Another company's record returns 404, not 403: saying "forbidden" would confirm the id exists.

Requires services:read.

Authorizations:
bearerAuth
path Parameters
serviceId
required
string

The service's id.

query Parameters
object
Example: fields[sites]=name,address

fields[type]=a,b narrows a representation to the fields named.

id is always returned, and anything named in include is kept whether or not it also appears here — an include that produced nothing while the response was a 200 is the most expensive kind of silence.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

List equipment

Equipment this company operates. It carries no rate of its own — pricing is the category's, so include=category is usually what you want.

Paged by cursor, id ascending — follow links.next until it is null. filter[updated_since] is the incremental export.

Includable: category.

Requires equipment:read.

Authorizations:
bearerAuth
query Parameters
cursor
string

An opaque cursor from links.next or links.prev. Do not construct or modify one: it encodes the sort values of the last row of the previous page and is only meaningful with the same filters and sort.

limit
integer [ 1 .. 200 ]
Default: 50

Rows per page. Clamped to the maximum rather than rejected.

sort
string
Example: sort=-updated_at,name

Comma-separated fields, - for descending: sort=-updated_at,name. At most 3. An unsortable field is a 422 listing what is sortable.

The primary key is always appended, so the order is total and paging cannot skip or repeat a row when values tie.

include
string
Example: include=contacts

Comma-separated related resources to embed, at most 5. A relation that is not included is absent from the response rather than null, so "not requested" is distinguishable from "requested, and empty".

object
Example: fields[sites]=name,address

fields[type]=a,b narrows a representation to the fields named.

id is always returned, and anything named in include is kept whether or not it also appears here — an include that produced nothing while the response was a 200 is the most expensive kind of silence.

object

Filterable: name, category_id, created_at, updated_at, plus filter[search] and filter[updated_since].

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    },
  • "links": {}
}

Fetch one record

A single record of the calling company.

Another company's record returns 404, not 403: saying "forbidden" would confirm the id exists.

Requires equipment:read.

Authorizations:
bearerAuth
path Parameters
equipmentId
required
string

The equipment's id.

query Parameters
include
string
Example: include=contacts

Comma-separated related resources to embed, at most 5. A relation that is not included is absent from the response rather than null, so "not requested" is distinguishable from "requested, and empty".

object
Example: fields[sites]=name,address

fields[type]=a,b narrows a representation to the fields named.

id is always returned, and anything named in include is kept whether or not it also appears here — an include that produced nothing while the response was a 200 is the most expensive kind of silence.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

List equipment categories

Equipment categories, and where equipment pricing lives.

In the legacy API categories were global with a separate per-company rate join; in v1 a category is the company's and carries its own defaults.

Paged by cursor, id ascending — follow links.next until it is null. filter[updated_since] is the incremental export.

Requires equipment-categories:read.

Authorizations:
bearerAuth
query Parameters
cursor
string

An opaque cursor from links.next or links.prev. Do not construct or modify one: it encodes the sort values of the last row of the previous page and is only meaningful with the same filters and sort.

limit
integer [ 1 .. 200 ]
Default: 50

Rows per page. Clamped to the maximum rather than rejected.

sort
string
Example: sort=-updated_at,name

Comma-separated fields, - for descending: sort=-updated_at,name. At most 3. An unsortable field is a 422 listing what is sortable.

The primary key is always appended, so the order is total and paging cannot skip or repeat a row when values tie.

object
Example: fields[sites]=name,address

fields[type]=a,b narrows a representation to the fields named.

id is always returned, and anything named in include is kept whether or not it also appears here — an include that produced nothing while the response was a 200 is the most expensive kind of silence.

object

Filterable: name, group, created_at, updated_at, plus filter[search] and filter[updated_since].

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    },
  • "links": {}
}

Fetch one record

A single record of the calling company.

Another company's record returns 404, not 403: saying "forbidden" would confirm the id exists.

Requires equipment-categories:read.

Authorizations:
bearerAuth
path Parameters
categoryId
required
string

The category's id.

query Parameters
object
Example: fields[sites]=name,address

fields[type]=a,b narrows a representation to the fields named.

id is always returned, and anything named in include is kept whether or not it also appears here — an include that produced nothing while the response was a 200 is the most expensive kind of silence.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

List consumables

Consumables — salt, sand, brine. Unlike equipment these carry their own default rates.

Paged by cursor, id ascending — follow links.next until it is null. filter[updated_since] is the incremental export.

Requires consumables:read.

Authorizations:
bearerAuth
query Parameters
cursor
string

An opaque cursor from links.next or links.prev. Do not construct or modify one: it encodes the sort values of the last row of the previous page and is only meaningful with the same filters and sort.

limit
integer [ 1 .. 200 ]
Default: 50

Rows per page. Clamped to the maximum rather than rejected.

sort
string
Example: sort=-updated_at,name

Comma-separated fields, - for descending: sort=-updated_at,name. At most 3. An unsortable field is a 422 listing what is sortable.

The primary key is always appended, so the order is total and paging cannot skip or repeat a row when values tie.

object
Example: fields[sites]=name,address

fields[type]=a,b narrows a representation to the fields named.

id is always returned, and anything named in include is kept whether or not it also appears here — an include that produced nothing while the response was a 200 is the most expensive kind of silence.

object

Filterable: name, group, created_at, updated_at, plus filter[search] and filter[updated_since].

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    },
  • "links": {}
}

Fetch one record

A single record of the calling company.

Another company's record returns 404, not 403: saying "forbidden" would confirm the id exists.

Requires consumables:read.

Authorizations:
bearerAuth
path Parameters
consumableId
required
string

The consumable's id.

query Parameters
object
Example: fields[sites]=name,address

fields[type]=a,b narrows a representation to the fields named.

id is always returned, and anything named in include is kept whether or not it also appears here — an include that produced nothing while the response was a 200 is the most expensive kind of silence.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}