Overview

The Pereira Index API is read-only and key-authenticated: send your key in the X-API-Key header on every request. Keys are never accepted in a query string.

Envelope. Every successful response is a JSON object: { data, data_as_of, pagination?, dispensing_window?, attribution?, caveat? }. Every error from the operations below is { "error": { "code", "message" } }, and their responses are never cached (Cache-Control: no-store). Dates are ISO 8601: timestamps in UTC, months as YYYY-MM. Only GET is documented, apart from POST /feeds/{partner}, which feed partners use with their own partner key; HEAD on the GET paths is answered like GET without a body and counted the same way. Paths and methods not listed here are not part of the API and may answer with a non-JSON 404 or 405.

data_as_of is the ISO 8601 timestamp of the newest data the response draws on, the latest of: for NHSBSA dispensing figures, when the newest release counted was fetched from the NHSBSA open data portal; for product master data (producers, products, pack sizes, sources, releases), the newest update among the rows returned; for availability, the newest snapshot or event returned. It is null when the response draws on nothing (an empty list).

NHSBSA figures (items dispensed in England, as released by the NHS Business Services Authority under the Open Government Licence), and NHSBSA release details such as dispensing_window, come with attribution and caveat, which must be shown wherever the figures or details are displayed or passed on. The attribution is that of the newest release counted in the response. Each month is counted from the newest release (largest FOI number) that holds it; the newest two months of a series are provisional because NHSBSA figures are lagged and revised. Figures from a release published without a stated licence are left out until its licence is confirmed.

Plans. A key works while its organisation has an active subscription (or is invoiced by agreement); otherwise the API answers 403 subscription_inactive. A payment that fails keeps access for 7 days while it is retried. An organisation may hold several keys at once, up to its plan's number of keys; its owner creates and revokes them on the account page, and every key shares the organisation's daily requests.

Rate limits. Requests are counted per organisation per UTC day, and the limit is set by your plan. Every request made with a valid key is counted once, including those answered 400, 404 or 429. Responses carry X-RateLimit-Limit and X-RateLimit-Remaining when your plan has a limit; over it the API answers 429 with Retry-After, the seconds to the next UTC midnight, when the count resets.

Responses are factual listings and figures: products, producers, pharmacies, availability and dispensed item counts. No price is returned. The API holds no patient data and no prescriber-level figures, and the public disclaimer in packages/core/src/copy.ts applies to every response.

Authentication and keys

An organisation's API key: "cbpm_" followed by 43 base64url characters. It is shown once, when it is created; only its SHA-256 hash and its first 12 characters are stored, so a lost key is replaced, not recovered. A revoked key is refused (invalid_api_key) from the moment it is revoked. Never send a key in a URL.

Send the key in the X-API-Key header of every request. Keep it in an environment variable or a secret store, as in the examples on this page:

export CBPM_API_KEY="cbpm_..."
curl -H "X-API-Key: $CBPM_API_KEY" "https://pereiraindex.co.uk/api/v1/products?page_size=10"

An organisation’s owner creates and revokes its keys on the account page, up to the plan’s number of keys; members of the organisation see the keys but cannot change them. A key is shown once, when it is created. To replace a key, create the new one, move your systems to it, then revoke the old one: a revoked key is refused from that moment. Every key of an organisation shares its daily requests.

Feed partners send availability files with a separate partner key: Feed partner key for POST /feeds/{partner} only: "cbpm_feed_" followed by 43 base64url characters, sent as Authorization: Bearer <key>. Issued once by the Pereira Index; only its SHA-256 hash is stored, and issuing a new key stops the previous one working.

Limits by plan

API limits of each plan
PlanAPI requests per day (UTC)API keys
Pilot1,0002
Pro10,0005
EnterpriseUnlimitedUnlimited

Requests are counted per organisation and reset at midnight UTC. Prices and everything else each plan includes are on the pricing page.

Attribution of NHSBSA figures

Responses that carry NHSBSA dispensing figures, or NHSBSA release details, include attribution and caveat. Wherever you display or pass on those figures, show both with them, as they come in the response. The attribution names the licence and year of the newest release counted, for example:

Contains public sector information licensed under the Open Government Licence v3.0. Dispensing data: Data Warehouse, NHSBSA Copyright 2026.

NHSBSA dispensing figures cover England only and count dispensed items, not patients or prescriptions. They are lagged by several months, revised retrospectively and unvalidated by NHSBSA, and their units are not standardised because these products are not on the NHSBSA drug database. One product can appear under many spellings, potency notations and pack sizes.

Figures cover England only, count items dispensed (not patients or prescriptions), are lagged and revised, and are not validated by NHSBSA. The API returns factual listings and figures only, and the disclaimer at the foot of this page applies to every response.

Endpoints

Products

GET /api/v1/products

Products in the master, searched and filtered, by canonical name.

Needs your organisation's API key in the X-API-Key header. Counted against your plan's daily requests.

Canonical products ordered by canonical name (case-insensitive), with their producer, declared cannabinoid content as stored, pack sizes, items dispensed (England, NHSBSA) in the dispensing window and the number of pharmacies listing each product. The same search, filters and pagination as the /products page. A blank parameter filters nothing; a malformed one is refused with 400; an unknown parameter is refused with 400. attribution and caveat are present whenever dispensing_window is not null (the window names NHSBSA months and a release), even when no product on the page has a figure.

Parameters of GET /api/v1/products
NameInRequiredTypeDescription
qqueryNostring, at most 200 charactersSearch text, at most 200 characters, split into words at whitespace. The search uses the first 8 words, each cut to its first 80 characters, and every one of those must appear in the canonical name, brand or producer name (case-insensitive); words after the eighth are ignored. Control characters other than whitespace are refused with 400. Example: example flower
producerqueryNostring, at most 200 characters, pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$A producer slug (exact). Example: example-producer
formatqueryNostring, one of flower, oil, cartridge, pastille, capsule, other
chemotypequeryNostring, one of I, II, III, unknown
countryqueryNostring, pattern ^[A-Za-z]{2}$Cultivation country, ISO 3166-1 alpha-2 (either case). Example: CA
pagequeryNointeger, 1 to 100,000, default 1
page_sizequeryNointeger, 1 to 100, default 50

Example request

curl -H "X-API-Key: $CBPM_API_KEY" "https://pereiraindex.co.uk/api/v1/products"
Responses of GET /api/v1/products
StatusMeaning
200One page of products.
400A malformed or unknown query parameter (invalid_parameter). Counted.
401No key (missing_api_key), or a key that is not valid or has been revoked (invalid_api_key). Not counted.
403The organisation holding the key has a plan this API does not know (plan_not_recognised), or has no active subscription: none, cancelled, or a payment overdue for more than 7 days (subscription_inactive). Not counted.
429Over your plan's requests for the UTC day (rate_limited). Counted.
500The request could not be completed (internal_error).
Example response (invented data)
{
  "data": [
    {
      "slug": "example-producer-example-flower-20",
      "canonical_name": "Example Flower 20",
      "brand": null,
      "format": "flower",
      "chemotype": "I",
      "thc_pct": "20.00",
      "cbd_pct": "1.00",
      "thc_mg_ml": null,
      "cbd_mg_ml": null,
      "irradiated": null,
      "cultivation_country": "CA",
      "status": "active",
      "producer": {
        "slug": "example-producer",
        "name": "Example Producer",
        "country": "CA"
      },
      "pack_sizes": [
        {
          "value": 10,
          "unit": "g",
          "label": "10 g"
        }
      ],
      "items_last_12_months": 1234,
      "pharmacies_listing": 0
    }
  ],
  "data_as_of": "2026-10-18T09:00:00.000Z",
  "pagination": {
    "page": 1,
    "page_size": 50,
    "total": 1,
    "total_pages": 1
  },
  "dispensing_window": {
    "from": "2025-06",
    "to": "2026-05",
    "months": 12,
    "newest_release": "FOI-00000"
  },
  "attribution": "Contains public sector information licensed under the Open Government Licence v3.0. Dispensing data: Data Warehouse, NHSBSA Copyright 2026.",
  "caveat": "NHSBSA dispensing figures cover England only and count dispensed items, not patients or prescriptions. They are lagged by several months, revised retrospectively and unvalidated by NHSBSA, and their units are not standardised because these products are not on the NHSBSA drug database. One product can appear under many spellings, potency notations and pack sizes."
}

GET /api/v1/products/{slug}

One product's facts as stored.

Needs your organisation's API key in the X-API-Key header. Counted against your plan's daily requests.

The product's facts exactly as the master holds them (a field the producer does not declare is null), its producer, pack sizes, items dispensed (England, NHSBSA) in the dispensing window and the number of pharmacies listing it. The monthly series is at /products/{slug}/dispensing and the listings at /products/{slug}/availability. attribution and caveat are present whenever dispensing_window is not null, even when nothing is mapped to the product yet (items_last_12_months null).

Parameters of GET /api/v1/products/{slug}
NameInRequiredTypeDescription
slugpathYesstring, at most 200 characters, pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$The product's slug. Example: example-producer-example-flower-20

Example request

curl -H "X-API-Key: $CBPM_API_KEY" "https://pereiraindex.co.uk/api/v1/products/example-producer-example-flower-20"
Responses of GET /api/v1/products/{slug}
StatusMeaning
200The product.
400A malformed or unknown query parameter (invalid_parameter). Counted.
401No key (missing_api_key), or a key that is not valid or has been revoked (invalid_api_key). Not counted.
403The organisation holding the key has a plan this API does not know (plan_not_recognised), or has no active subscription: none, cancelled, or a payment overdue for more than 7 days (subscription_inactive). Not counted.
404No product or producer has this slug (not_found). Counted.
429Over your plan's requests for the UTC day (rate_limited). Counted.
500The request could not be completed (internal_error).
Example response (invented data)
{
  "data": {
    "slug": "example-producer-example-flower-20",
    "canonical_name": "Example Flower 20",
    "brand": null,
    "format": "flower",
    "chemotype": "I",
    "thc_pct": "20.00",
    "cbd_pct": "1.00",
    "thc_mg_ml": null,
    "cbd_mg_ml": null,
    "irradiated": null,
    "cultivation_country": "CA",
    "status": "active",
    "producer": {
      "slug": "example-producer",
      "name": "Example Producer",
      "country": "CA",
      "website": null,
      "type": "cultivator"
    },
    "pack_sizes": [
      {
        "value": 10,
        "unit": "g",
        "label": "10 g"
      }
    ],
    "items_last_12_months": 1234,
    "pharmacies_listing": 0
  },
  "data_as_of": "2026-10-18T09:00:00.000Z",
  "dispensing_window": {
    "from": "2025-06",
    "to": "2026-05",
    "months": 12,
    "newest_release": "FOI-00000"
  },
  "attribution": "Contains public sector information licensed under the Open Government Licence v3.0. Dispensing data: Data Warehouse, NHSBSA Copyright 2026.",
  "caveat": "NHSBSA dispensing figures cover England only and count dispensed items, not patients or prescriptions. They are lagged by several months, revised retrospectively and unvalidated by NHSBSA, and their units are not standardised because these products are not on the NHSBSA drug database. One product can appear under many spellings, potency notations and pack sizes."
}

GET /api/v1/products/{slug}/dispensing

Items dispensed (England, NHSBSA) per month for one product, with each month's release.

Needs your organisation's API key in the X-API-Key header. Counted against your plan's daily requests.

The product's items per month, from its first month with items to the newest month with data (months without items are 0), each with the FOI id of the release it is counted from and a provisional flag on the newest two months. Matched items only: NHSBSA lines whose free-text description is not yet mapped to the product are not counted. Items, not patients or prescriptions. attribution and caveat are present whenever any release is counted, even for a series with no months yet.

Parameters of GET /api/v1/products/{slug}/dispensing
NameInRequiredTypeDescription
slugpathYesstring, at most 200 characters, pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$The product's slug. Example: example-producer-example-flower-20

Example request

curl -H "X-API-Key: $CBPM_API_KEY" "https://pereiraindex.co.uk/api/v1/products/example-producer-example-flower-20/dispensing"
Responses of GET /api/v1/products/{slug}/dispensing
StatusMeaning
200The product's monthly series.
400A malformed or unknown query parameter (invalid_parameter). Counted.
401No key (missing_api_key), or a key that is not valid or has been revoked (invalid_api_key). Not counted.
403The organisation holding the key has a plan this API does not know (plan_not_recognised), or has no active subscription: none, cancelled, or a payment overdue for more than 7 days (subscription_inactive). Not counted.
404No product or producer has this slug (not_found). Counted.
429Over your plan's requests for the UTC day (rate_limited). Counted.
500The request could not be completed (internal_error).
Example response (invented data)
{
  "data": {
    "subject": {
      "kind": "product",
      "slug": "example-producer-example-flower-20",
      "name": "Example Flower 20"
    },
    "items_last_12_months": 210,
    "total_items": 210,
    "releases": [
      "FOI-00000 (Mar 2026 to May 2026)"
    ],
    "series": [
      {
        "month": "2026-03",
        "items": 60,
        "release": "FOI-00000",
        "provisional": false
      },
      {
        "month": "2026-04",
        "items": 70,
        "release": "FOI-00000",
        "provisional": true
      },
      {
        "month": "2026-05",
        "items": 80,
        "release": "FOI-00000",
        "provisional": true
      }
    ]
  },
  "data_as_of": "2026-10-18T09:00:00.000Z",
  "dispensing_window": {
    "from": "2025-06",
    "to": "2026-05",
    "months": 12,
    "newest_release": "FOI-00000"
  },
  "attribution": "Contains public sector information licensed under the Open Government Licence v3.0. Dispensing data: Data Warehouse, NHSBSA Copyright 2026.",
  "caveat": "NHSBSA dispensing figures cover England only and count dispensed items, not patients or prescriptions. They are lagged by several months, revised retrospectively and unvalidated by NHSBSA, and their units are not standardised because these products are not on the NHSBSA drug database. One product can appear under many spellings, potency notations and pack sizes."
}

Producers

GET /api/v1/producers

Every producer, by name.

Needs your organisation's API key in the X-API-Key header. Counted against your plan's daily requests.

Every producer in the master ordered by name (case-insensitive), never by volume, with its number of products and items dispensed (England, NHSBSA) in the dispensing window. Not paginated. attribution and caveat are present whenever dispensing_window is not null, even when no producer has a figure.

Example request

curl -H "X-API-Key: $CBPM_API_KEY" "https://pereiraindex.co.uk/api/v1/producers"
Responses of GET /api/v1/producers
StatusMeaning
200The producers.
400A malformed or unknown query parameter (invalid_parameter). Counted.
401No key (missing_api_key), or a key that is not valid or has been revoked (invalid_api_key). Not counted.
403The organisation holding the key has a plan this API does not know (plan_not_recognised), or has no active subscription: none, cancelled, or a payment overdue for more than 7 days (subscription_inactive). Not counted.
429Over your plan's requests for the UTC day (rate_limited). Counted.
500The request could not be completed (internal_error).
Example response (invented data)
{
  "data": [
    {
      "slug": "example-producer",
      "name": "Example Producer",
      "country": "CA",
      "website": null,
      "type": "cultivator",
      "products": 1,
      "items_last_12_months": 1234
    }
  ],
  "data_as_of": "2026-10-18T09:00:00.000Z",
  "dispensing_window": {
    "from": "2025-06",
    "to": "2026-05",
    "months": 12,
    "newest_release": "FOI-00000"
  },
  "attribution": "Contains public sector information licensed under the Open Government Licence v3.0. Dispensing data: Data Warehouse, NHSBSA Copyright 2026.",
  "caveat": "NHSBSA dispensing figures cover England only and count dispensed items, not patients or prescriptions. They are lagged by several months, revised retrospectively and unvalidated by NHSBSA, and their units are not standardised because these products are not on the NHSBSA drug database. One product can appear under many spellings, potency notations and pack sizes."
}

GET /api/v1/producers/{slug}/dispensing

Items dispensed (England, NHSBSA) per month for one producer, all its products summed.

Needs your organisation's API key in the X-API-Key header. Counted against your plan's daily requests.

The producer's items per month across all its products, in the same shape and under the same rules as /products/{slug}/dispensing.

Parameters of GET /api/v1/producers/{slug}/dispensing
NameInRequiredTypeDescription
slugpathYesstring, at most 200 characters, pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$The producer's slug. Example: example-producer

Example request

curl -H "X-API-Key: $CBPM_API_KEY" "https://pereiraindex.co.uk/api/v1/producers/example-producer/dispensing"
Responses of GET /api/v1/producers/{slug}/dispensing
StatusMeaning
200The producer's monthly series.
400A malformed or unknown query parameter (invalid_parameter). Counted.
401No key (missing_api_key), or a key that is not valid or has been revoked (invalid_api_key). Not counted.
403The organisation holding the key has a plan this API does not know (plan_not_recognised), or has no active subscription: none, cancelled, or a payment overdue for more than 7 days (subscription_inactive). Not counted.
404No product or producer has this slug (not_found). Counted.
429Over your plan's requests for the UTC day (rate_limited). Counted.
500The request could not be completed (internal_error).
Example response (invented data)
{
  "data": {
    "subject": {
      "kind": "producer",
      "slug": "example-producer",
      "name": "Example Producer"
    },
    "items_last_12_months": 150,
    "total_items": 150,
    "releases": [
      "FOI-00000 (Apr 2026 to May 2026)"
    ],
    "series": [
      {
        "month": "2026-04",
        "items": 70,
        "release": "FOI-00000",
        "provisional": true
      },
      {
        "month": "2026-05",
        "items": 80,
        "release": "FOI-00000",
        "provisional": true
      }
    ]
  },
  "data_as_of": "2026-10-18T09:00:00.000Z",
  "dispensing_window": {
    "from": "2025-06",
    "to": "2026-05",
    "months": 12,
    "newest_release": "FOI-00000"
  },
  "attribution": "Contains public sector information licensed under the Open Government Licence v3.0. Dispensing data: Data Warehouse, NHSBSA Copyright 2026.",
  "caveat": "NHSBSA dispensing figures cover England only and count dispensed items, not patients or prescriptions. They are lagged by several months, revised retrospectively and unvalidated by NHSBSA, and their units are not standardised because these products are not on the NHSBSA drug database. One product can appear under many spellings, potency notations and pack sizes."
}

Availability

GET /api/v1/products/{slug}/availability

The latest snapshot of each listing of the product.

Needs your organisation's API key in the X-API-Key header. Counted against your plan's daily requests.

One row per listing of the product's pack sizes at a public source (a pharmacy or importer feed partner, or an approved public source), from the latest snapshot of that listing. current is false when the snapshot is more than 7 days old: the state is then as of observed_at, not current. A pharmacy lists the product when one of its listings is current and not delisted. Empty until the first partner feed delivers. No price is returned. No NHSBSA figures, so no attribution.

Parameters of GET /api/v1/products/{slug}/availability
NameInRequiredTypeDescription
slugpathYesstring, at most 200 characters, pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$The product's slug. Example: example-producer-example-flower-20

Example request

curl -H "X-API-Key: $CBPM_API_KEY" "https://pereiraindex.co.uk/api/v1/products/example-producer-example-flower-20/availability"
Responses of GET /api/v1/products/{slug}/availability
StatusMeaning
200The product's listings.
400A malformed or unknown query parameter (invalid_parameter). Counted.
401No key (missing_api_key), or a key that is not valid or has been revoked (invalid_api_key). Not counted.
403The organisation holding the key has a plan this API does not know (plan_not_recognised), or has no active subscription: none, cancelled, or a payment overdue for more than 7 days (subscription_inactive). Not counted.
404No product or producer has this slug (not_found). Counted.
429Over your plan's requests for the UTC day (rate_limited). Counted.
500The request could not be completed (internal_error).
Example response (invented data)
{
  "data": {
    "product": {
      "slug": "example-producer-example-flower-20",
      "canonical_name": "Example Flower 20"
    },
    "pharmacies_listing": 1,
    "listings": [
      {
        "source": {
          "slug": "example-pharmacy",
          "name": "Example Pharmacy",
          "type": "pharmacy"
        },
        "pack": {
          "value": 10,
          "unit": "g",
          "label": "10 g"
        },
        "availability": "in_stock",
        "label": "In stock",
        "observed_at": "2026-10-18T09:00:00.000Z",
        "current": true
      }
    ]
  },
  "data_as_of": "2026-10-18T09:00:00.000Z"
}

GET /api/v1/events

Availability changes since a timestamp, oldest first.

Needs your organisation's API key in the X-API-Key header. Counted against your plan's daily requests.

Availability events (a listing's state changing between two snapshots) of public listings at or after since, ordered by at then id, at most limit per page. To read the next page, pass pagination.next.since and pagination.next.after_id back as since and after_id; pagination.next is null on the last page. Empty until the first partner feed delivers. No NHSBSA figures, so no attribution.

Parameters of GET /api/v1/events
NameInRequiredTypeDescription
sincequeryYesstringISO 8601 date or timestamp. A date alone is midnight UTC; a time without an offset is UTC. An offset may be at most 14:00 either way. Fractions of a second keep up to 6 digits, so an event's own at can be passed back. Example: 2026-10-18T00:00:00Z
after_idqueryNointeger, at least 1With since, only events after (since, after_id); pass pagination.next.after_id.
limitqueryNointeger, 1 to 1,000, default 100

Example request

curl -H "X-API-Key: $CBPM_API_KEY" "https://pereiraindex.co.uk/api/v1/events?since=2026-10-18T00%3A00%3A00Z"
Responses of GET /api/v1/events
StatusMeaning
200One page of events.
400A malformed or unknown query parameter (invalid_parameter). Counted.
401No key (missing_api_key), or a key that is not valid or has been revoked (invalid_api_key). Not counted.
403The organisation holding the key has a plan this API does not know (plan_not_recognised), or has no active subscription: none, cancelled, or a payment overdue for more than 7 days (subscription_inactive). Not counted.
429Over your plan's requests for the UTC day (rate_limited). Counted.
500The request could not be completed (internal_error).
Example response (invented data)
{
  "data": [
    {
      "id": 1,
      "at": "2026-10-18T09:00:00.000000Z",
      "from": "in_stock",
      "to": "out_of_stock",
      "from_label": "In stock",
      "to_label": "Out of stock",
      "product": {
        "slug": "example-producer-example-flower-20",
        "canonical_name": "Example Flower 20"
      },
      "pack": {
        "value": 10,
        "unit": "g",
        "label": "10 g"
      },
      "source": {
        "slug": "example-pharmacy",
        "name": "Example Pharmacy",
        "type": "pharmacy"
      }
    }
  ],
  "data_as_of": "2026-10-18T09:00:00.000Z",
  "pagination": {
    "limit": 100,
    "next": null
  }
}

Sources

GET /api/v1/sources

Where the data comes from.

Needs your organisation's API key in the X-API-Key header. Counted against your plan's daily requests.

The public availability sources (feed partners' sources and approved public sources, by name) and every NHSBSA release fetched, newest (largest FOI number) first, each with its own attribution and whether it was published with a stated licence. Release coverage is the first and last month of its monthly lines (null for a release that holds no monthly lines). The envelope attribution is that of the newest release listed.

Example request

curl -H "X-API-Key: $CBPM_API_KEY" "https://pereiraindex.co.uk/api/v1/sources"
Responses of GET /api/v1/sources
StatusMeaning
200Sources and releases.
400A malformed or unknown query parameter (invalid_parameter). Counted.
401No key (missing_api_key), or a key that is not valid or has been revoked (invalid_api_key). Not counted.
403The organisation holding the key has a plan this API does not know (plan_not_recognised), or has no active subscription: none, cancelled, or a payment overdue for more than 7 days (subscription_inactive). Not counted.
429Over your plan's requests for the UTC day (rate_limited). Counted.
500The request could not be completed (internal_error).
Example response (invented data)
{
  "data": {
    "sources": [
      {
        "slug": "example-pharmacy",
        "name": "Example Pharmacy",
        "type": "pharmacy",
        "base_url": "https://pharmacy.example",
        "allowed": false,
        "feed": true,
        "last_ok_at": "2026-10-18T09:00:00.000Z"
      }
    ],
    "releases": [
      {
        "foi_id": "FOI-00000",
        "title": "Example release title",
        "source_url": "https://opendata.example/dataset/foi-00000",
        "covers_from": "2025-01",
        "covers_to": "2026-05",
        "downloaded_at": "2026-10-18T09:00:00.000Z",
        "attribution": "Contains public sector information licensed under the Open Government Licence v3.0. Dispensing data: Data Warehouse, NHSBSA Copyright 2026.",
        "stated_licence": true
      }
    ]
  },
  "data_as_of": "2026-10-18T09:00:00.000Z",
  "attribution": "Contains public sector information licensed under the Open Government Licence v3.0. Dispensing data: Data Warehouse, NHSBSA Copyright 2026.",
  "caveat": "NHSBSA dispensing figures cover England only and count dispensed items, not patients or prescriptions. They are lagged by several months, revised retrospectively and unvalidated by NHSBSA, and their units are not standardised because these products are not on the NHSBSA drug database. One product can appear under many spellings, potency notations and pack sizes."
}

Feeds

POST /api/v1/feeds/{partner}

Send a partner availability feed.

Feed partners only, with the partner key in the Authorization header. Not counted against any plan.

For feed partners only, with the partner key the Pereira Index issued (not an organisation's API key): Authorization: Bearer <partner key>. The body is the feed file itself, as text/csv (UTF-8, header row, comma-separated) or application/json (an array of objects), at most 5 MB and 10,000 rows. The columns, accepted values and examples are in the feed specification (docs/FEED_SPEC.md, served at /feeds/spec on the site).

A file with any column outside the specification is refused whole (422) and its content is not stored: a feed carries product data only, never patient data. Otherwise the file is stored and validated row by row; accepted rows go on even when others are rejected, and the response lists every rejected row with the field and the reason. Rows become listings only once the partner's feed agreement is recorded (status: held until then). When anything is refused or rejected the same report is emailed to the partner's contact.

At most one submission per minute per partner (429 with Retry-After). Not counted against any organisation's API plan.

Parameters of POST /api/v1/feeds/{partner}
NameInRequiredTypeDescription
partnerpathYesstring, at most 200 characters, pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$The feed partner's slug, as agreed with the Pereira Index. Example: example-pharmacy

Request body: text/csv or application/json.

Example request

curl -X POST -H "Authorization: Bearer $CBPM_FEED_KEY" -H "Content-Type: text/csv" --data-binary @feed.csv "https://pereiraindex.co.uk/api/v1/feeds/example-pharmacy"
Responses of POST /api/v1/feeds/{partner}
StatusMeaning
200The submission was recorded; the report, with any rejected rows.
401No partner key (missing_partner_key), or a key that is not this partner's (invalid_partner_key, also for a partner that does not exist).
413The body is larger than 5 MB (payload_too_large). Nothing is recorded.
415The body is not text/csv or application/json in UTF-8 (unsupported_media_type). Nothing is recorded.
422The file was refused whole (file_refused): empty, not UTF-8, unreadable, a column outside the specification (its content is not stored), a required column missing, no rows or more than 10,000. The message says why and what to do, and names the submission.
429A submission from this partner arrived less than a minute ago (rate_limited). Nothing is recorded.
500The request could not be completed (internal_error).
Example response (invented data)
{
  "data": {
    "submission_id": 12,
    "status": "imported",
    "received_at": "2026-10-24T09:00:00.000Z",
    "channel": "api",
    "rows": 2,
    "accepted_rows": 1,
    "rejected_rows": 1,
    "rejected": [
      {
        "row": 2,
        "field": "pack_unit",
        "reason": "pack_unit must be one of g, ml, units (got \"kg\")"
      }
    ],
    "as_of": "2026-10-24",
    "listings": {
      "created": 1,
      "seen": 1,
      "newly_matched": 1,
      "awaiting_review": 0,
      "snapshots": 1
    },
    "report": "Pereira Index feed report for Example Pharmacy ...",
    "specification": "https://pereiraindex.example/feeds/spec"
  },
  "data_as_of": "2026-10-24T09:00:00.000Z"
}

Operations

GET /api/health

Liveness check for the web app.

Needs no key and is not counted.

Returns immediately without touching the database. Never cached. Needs no key and is not counted.

Example request

curl "https://pereiraindex.co.uk/api/health"
Responses of GET /api/health
StatusMeaning
200The web app is running.
Example response (invented data)
{
  "ok": true,
  "service": "cbpm-registry-web",
  "time": "2026-10-18T09:00:00.000Z"
}

Error codes

Every error is { "error": { "code", "message" } }, never cached. The message is a sentence for a person and never contains your key. The codes: