# Pereira Index partner feed specification Version 1, 24 Oct 2026. This is the file a pharmacy or importer sends the Pereira Index on a schedule (for example weekly) so that its products appear in the registry with up-to-date availability. It is published at `/feeds/spec` on the Pereira Index site and kept in the repository as `docs/FEED_SPEC.md`. **A feed carries product data only, never patient data.** No patient names, initials, dates of birth, NHS numbers, prescription or order references, prescriber names, addresses or notes. A file with any column that is not listed below is refused whole and nothing from it is stored; we tell you which columns to remove. ## What to send One row per product you list (per SKU), describing that product on the day in `as_of`. Send every product you list, in stock or not, each time. | Column | Required | What it holds | Accepted values | |---|---|---|---| | `partner_sku` | yes | Your own stable code for the product. It must stay the same from one file to the next. | Text, at most 100 characters, each SKU once per file | | `product_name` | yes | The product name as you print it | Text, at most 200 characters | | `brand` | no | The brand or producer | Text, at most 100 characters | | `format` | yes | The product format | `flower`, `oil`, `cartridge`, `pastille`, `capsule`, `other` | | `thc` | no | Declared THC content as printed on the label | Text such as `22%`, `<1%` or `10mg/ml` | | `cbd` | no | Declared CBD content as printed on the label | Text such as `1%` or `20mg/ml` | | `pack_size` | yes | The pack size, as a number | A positive number such as `10` or `2.5`, at most 3 decimal places, no unit | | `pack_unit` | yes | The unit of `pack_size` | `g`, `ml`, `units` (use `units` for capsules, pastilles and other counted items) | | `price_pence` | no | Your price in whole pence | A whole number such as `4500` for £45.00, no £ sign or decimal point | | `availability` | yes | Whether you can supply it | `in_stock`, `low_stock`, `out_of_stock` | | `as_of` | yes | The date the row is true for | `YYYY-MM-DD`, such as `2026-10-24`; at most one day in the future | Column names may be in any order and any case. Values are trimmed; a blank optional value means "not stated". Values for `format`, `pack_unit` and `availability` are accepted in any case. We record the declared THC and CBD exactly as you write them; we read them where we can, and text we cannot read is kept, never an error. Prices are kept for our own data checks and are not shown on the public site. ## Formats **CSV.** UTF-8 text (in Excel, "Save as: CSV UTF-8"), a header row with the column names, then one row per product, separated by commas. A value that contains a comma or a line break must be wrapped in double quotes, and a double quote inside such a value is written twice. A leading byte order mark and Windows line endings are fine. ``` partner_sku,product_name,brand,format,thc,cbd,pack_size,pack_unit,price_pence,availability,as_of EX-001,Example Flower,Example Brand,flower,22%,<1%,10,g,4500,in_stock,2026-10-24 EX-002,"Example Oil, full spectrum",Example Brand,oil,10mg/ml,20mg/ml,30,ml,,low_stock,2026-10-24 EX-003,Example Pastilles,Example Brand,pastille,,,30,units,3900,out_of_stock,2026-10-24 ``` **JSON.** UTF-8, one array with one object per product. Keys are the column names above; numbers may be sent as numbers or text, and `null` means "not stated". Every value must be text, a number or `null`: an object, a list or `true`/`false` as a value refuses the whole file (see File checks). ```json [ { "partner_sku": "EX-001", "product_name": "Example Flower", "brand": "Example Brand", "format": "flower", "thc": "22%", "cbd": "<1%", "pack_size": 10, "pack_unit": "g", "price_pence": 4500, "availability": "in_stock", "as_of": "2026-10-24" } ] ``` ## Limits - At most 5 MB and 10,000 rows per file. Split a larger catalogue into several files. - Through the API, at most one file per minute. ## How to send it 1. **Upload page.** Sign in at `/login` with the email address we added to your organisation, then open `/account/feeds`, choose the file and press Upload. The page shows the result straight away and your last 10 submissions with any row errors. 2. **API.** `POST /api/v1/feeds/` with the file as the request body, `Content-Type: text/csv` or `application/json`, and the partner key we gave you in `Authorization: Bearer `. The key is shown to us once when it is made and only a hash of it is stored; if it is lost we issue a new one and the old one stops working. Never put the key in a URL or an email. Example: ``` curl -X POST https:///api/v1/feeds/ \ -H "Authorization: Bearer $PARTNER_KEY" \ -H "Content-Type: text/csv" \ --data-binary @stock.csv ``` The response is JSON: `{ "data": { "submission_id", "status", "rows", "accepted_rows", "rejected_rows", "rejected": [ { "row", "field", "reason" } ], "report", ... }, "data_as_of" }`. Refusals: 401 (no key, or not your key), 413 (over 5 MB), 415 (not CSV or JSON), 422 (file refused, with the reason), 429 (less than a minute since your last file; wait for the seconds in `Retry-After`). The full description is in the API specification (`apps/web/openapi.yaml`). 3. **Email.** Emailing a file to a feed mailbox is not open yet; until it is, send it to your contact at the Pereira Index, who records it for you. ## What happens to a file 1. **File checks.** A file that is empty, larger than 5 MB, not UTF-8, unreadable, has a column outside this specification, names a column twice, lacks a required column, has a row with more values than the header has columns, has a JSON value that is an object, a list or `true`/`false` (these can carry fields this specification does not name), has no rows or has more than 10,000 rows is **refused whole**. Nothing from it is stored except its column names and the reason. 2. **Row checks.** Every other file is stored as sent and checked row by row. A row is rejected when a required value is missing or a value is not accepted (a format not in the list, a pack size that is not a positive number, a date that is not `YYYY-MM-DD` or is more than a day ahead, and so on), or when its `partner_sku` appears on more than one row of the file (every such row is rejected, since we cannot tell which is right). **The other rows go on.** 3. **Listings.** Once your feed agreement is recorded with us, each accepted row updates your listing for that SKU, with its availability on the `as_of` date. We match each product to our product register; a product we cannot match with confidence waits for our review and is never guessed. Before the agreement is recorded, files are checked and kept but nothing is listed. ## The report After every file you get a report: on the upload page, in the API response, and **by email to your feed contact whenever anything was refused or rejected**. It looks like this: ``` Pereira Index feed report for Example Pharmacy Submission 12, received 24 Oct 2026 14:05 UTC by upload. Outcome: imported. 2 of 3 rows accepted, 1 rejected. Rows describe 24 Oct 2026. Listings: 2 new, 2 seen, 2 availability snapshots. Rejected rows (fix these and send them again in your next file): - Row 2, format: format must be one of flower, oil, cartridge, pastille, capsule, other (got "resin") The feed specification: https:///feeds/spec A feed carries product data only, never patient data. ``` Row 1 is the first row after the header (CSV) or the first object (JSON). The email lists at most 50 row errors, the upload page up to 200, and the API response lists them all. ## Questions Ask your contact at the Pereira Index. Changes to this specification are announced to every partner before they apply, and a new version keeps accepting files written to this one.