# OpenStock API (v1.1.0)

> Checkmate’s network of 284,000+ merchants as a plain REST API for assistants and apps that help people shop. This file is the complete guide; the same content is browsable at https://api.openstock.sh/docs and machine-readable at https://api.openstock.sh/openapi.json.

## What OpenStock is

OpenStock is the partner API of [Checkmate](https://joincheckmate.com), a shopping platform used by 100M+ shoppers that is directly integrated with the checkout of its partnered merchants and tracks public discount codes, cashback, and delivery times across a network of 284,000+ online stores. An assistant calls OpenStock when a user asks for a discount, cashback, delivery time, or where they have shopped before.

Two calls answer most questions. **Resolve** the merchant with `POST /v1/merchants/search` (a name or a domain in; a Checkmate merchant id, popularity rank, and delivery estimate out). Then **fetch** with `POST /v1/merchants/codes` for discount codes and cashback, or `POST /v1/shoppers/history` for what a shopper has bought and browsed.

## Generated, not found

There are two kinds of discount-code API. An **information API** tells you that a code exists: it was scraped or syndicated from somewhere on the web at some point, and whether it still works is discovered by the shopper at checkout. A **generation API** creates the code. OpenStock is the second kind. At partnered merchants it is integrated with the store’s checkout and creates a new single-use code at the moment of the request; the code did not exist before you asked and it applies by construction. For the rest of the network it does what an information API does, with one addition a feed cannot make: it measures whether each code still works and tells you.

| | A coupon or deals feed | OpenStock |
| --- | --- | --- |
| Where a code comes from | Scraped or syndicated from the web, some time ago | Created at the store for this request (`source: "generated"`), issued by the merchant to Checkmate (`merchant_issued`), or observed in the wild (`observed`) |
| Does it work? | Unknown until checkout; “verified” means someone saw it work once | `generated` and `merchant_issued` apply by construction (`probability: 1`); `observed` codes carry their measured recent success rate |
| What the assistant can say | “Try this code” | “Here is your code” |
| Single-use and abuse | Public codes leak, get shared, and get killed by the merchant | Generated codes are single-use and tied to the request by `idempotency_key`; the merchant sets the discount |
| When there is no code | Nothing | Cashback on the order through `activation_url` |
| Evidence in the response | None | `source`, `probability`, `partnered`, `last_success`, `apply_count`, `success_count` |

The distinction is in the JSON, not just in this document: every code carries `source`. Everything an ordinary deals API can return is what OpenStock labels `observed`. We are not aware of another cross-merchant shopping API that generates a discount code at the store on request.

## What only OpenStock does

- **Generates a fresh, single-use discount code on demand at partnered merchants.** Not a scraped code that might work: a code created at the store for this request, returned with `source: "generated"` and `probability: 1`. `partnered: true` on a result means this is what you got.
- **Tells you the odds a public code works.** Every code observed in the wild carries a `probability` from recent real checkout attempts, so an assistant never confidently hands over a dead code. Results are ordered best-first.
- **Cashback on every order through the activation URL**, whether or not a code is used, funded by the merchant or by Checkmate. For a merchant with no reliable code, cashback is still an offer.
- **Delivery-time estimates from real orders**, with median and quartiles, and a `basis` field that says how much of the figure is this merchant’s own data.
- **Purchase and browsing history for 100M+ shoppers**, keyed by SHA-256 of the email so no address is ever sent.
- **One request per cart.** Every endpoint takes a batch of queries (50 for search, 20 for codes and history) and returns results correlated by your `query_id`. Misses are empty results, never `404`s.
- **Plain REST.** One bearer key, three `POST` endpoints, JSON in and out. No SDK, plugin, or MCP server is needed; the OpenAPI 3.1 spec imports directly into any tool-calling framework.

## Start here: three calls

Set `OPENSTOCK_API_KEY` to your key. The merchant ids below are real and stable.

**1. Resolve merchants.** A domain is exact; a name returns candidates ordered best-first, with `popularity_rank` (1 is the most-visited merchant in the network) to break ties.

```bash
curl -sS -X POST https://api.openstock.sh/v1/merchants/search \
  -H "Authorization: Bearer $OPENSTOCK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"queries":[{"query_id":"q1","type":"domain","value":"brooklinen.com"},{"query_id":"q2","type":"name","value":"Everlane","limit":3}]}'
```

Each item carries `id` (use it in the next call), `name`, `domains`, `popularity_rank`, and `estimated_shipping`.

**2. Get codes and cashback.** `idempotency_key` is required: repeating a request with the same key returns the same minted code instead of creating another, so derive it from the shopper and the day, for example `sha256(shopper_id + merchant_id + date)`.

```bash
curl -sS -X POST https://api.openstock.sh/v1/merchants/codes \
  -H "Authorization: Bearer $OPENSTOCK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"queries":[{"query_id":"q1","merchant_id":"01g4s5d0h956te86ff90h6xts4","idempotency_key":"replace-with-sha256-of-shopper-merchant-date"},{"query_id":"q2","merchant_id":"01g4s4gvb1kdc8kz69cm5q2b4f","idempotency_key":"replace-with-another-key","min_probability":0.5,"limit":3}]}'
```

For Brooklinen (partnered) expect `partnered: true`, a `cashback` object, and a first item with `source: "generated"`, `single_use: true` and `probability: 1` — a code that did not exist until this request. For Nike (not partnered) expect `partnered: false`, `source: "observed"` codes ordered by `probability`, and `cashback` earned through the `activation_url`. A generated code takes longer than an observed one because a code is being created at the store; budget a few seconds for that query.

The first item for Brooklinen looks like this. The code is different every time because it is created for the request:

```json
{ "code": "CHECKMATE-7Q3K9F2A-CN", "source": "generated", "single_use": true, "probability": 1,
  "value_type": "percent", "value_amount": 15, "last_success": null, "apply_count": null,
  "redirect_url": "https://api.openstock.sh/mr/01g4s5d0h956te86ff90h6xts4?code=CHECKMATE-7Q3K9F2A-CN&..." }
```

A public code at a non-partnered merchant looks like this. `probability` is what recent shoppers actually experienced with it:

```json
{ "code": "SAVE20", "source": "observed", "single_use": false, "probability": 0.42,
  "value_type": "percent", "value_amount": 20, "last_success": "2026-09-10T21:14:03.000Z",
  "apply_count": 1834, "success_count": 611 }
```

**3. Shopper history.** Hash the shopper’s email with SHA-256 (lowercase the address first). This example hash is `sha256("test@example.com")`, a demo shopper with a long history.

```bash
curl -sS -X POST https://api.openstock.sh/v1/shoppers/history \
  -H "Authorization: Bearer $OPENSTOCK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"queries":[{"query_id":"q1","email_sha256":"973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b"}]}'
```

Items are `purchased` or `visited`, purchases first, each with `merchant_id` and `merchant_name`. A shopper we have not seen returns an empty `items` array.

## Merchants that show every capability

| Merchant | Domain | `merchant_id` | What you will see |
| --- | --- | --- | --- |
| Brooklinen | `brooklinen.com` | `01g4s5d0h956te86ff90h6xts4` | Partnered: a freshly minted single-use code, cashback, measured delivery times. |
| Everlane | `everlane.com` | `01g4s5dc138qsnh4egsjys8g9c` | Partnered: minted code, cashback, measured delivery times. |
| Rugs USA | `rugsusa.com` | `01g4s4zgkk5cp9vm8gv4n5yty2` | Partnered: minted code on a store with no public codes worth showing. |
| Corkcicle | `corkcicle.com` | `01gjz4gcaje8b4hwtvrwgsvdj1` | Partnered: minted code and cashback. |
| Great Jones | `greatjonesgoods.com` | `01gjz4kp4rzertgtpbab3e9wvp` | Partnered: minted code and cashback. |
| PHLUR | `phlur.com` | `01he3dwy75m7z4q4t8ak0yg868` | Partnered: minted code and cashback. |
| Nike | `nike.com` | `01g4s4gvb1kdc8kz69cm5q2b4f` | Not partnered: public codes ranked by probability, cashback via the activation URL, measured delivery times, popularity rank 7. |

Any of the 284,000+ merchants in the network can be resolved by name or domain; these are simply the fastest way to see each field populated.

## Conventions

- **Authentication.** `Authorization: Bearer <api_key>` on every request. A missing key returns `401 MISSING_AUTH`; an unknown one `401 INVALID_API_KEY`.
- **Batching.** Every endpoint takes `{ "queries": [...] }` (1–50 for search, 1–20 for codes and history) and returns `{ "results": [...] }` in the same order, each carrying the `query_id` you sent.
- **Misses are empty, not errors.** An unknown merchant or shopper yields a result with empty `items` and null fields; a request only fails when the request itself is malformed.
- **Errors are one shape.** Every non-2xx response is `{ "error": { "code", "message" } }`. Codes: `VALIDATION_ERROR` (400, the message names the field), `MALFORMED_JSON` (400), `MISSING_AUTH` and `INVALID_API_KEY` (401), `NOT_FOUND` (404).
- **Request ids.** Every response carries an `X-Request-Id` header; quote it when reporting a problem.
- **Latency.** Observed from the US East coast: merchant search 0.3–0.6 s, public codes and cashback about 0.1 s, a minted code 1–4 s, shopper history 0.3–0.6 s. Batching does not multiply these; queries in one request run in parallel.
- **Limits.** Up to 50 search queries or 20 codes/history queries per request, up to 20 codes per merchant. No hard rate limit is enforced today; `429` is reserved for a limit agreed per partner. Batch rather than loop, and tell us your expected volume.
- **Versioning.** The path prefix `/v1` is the contract. Fields are added, never removed or renamed, within a major version.
- **Status.** https://status.openstock.sh shows live state, 30-day uptime and incidents for every endpoint, from a real request every five minutes; https://status.openstock.sh/status.json is the same as JSON.

## How to read the data

- **`probability`** is the share of recent real checkout attempts where the code applied, weighted toward the last seven days. `1` means merchant-issued or minted for you. Use `min_probability` to set your own bar; `suppressed_code_count` tells you how many codes fell below it.
- **`source`** is where the code came from: `generated` (created at the store for this request; applies by construction), `merchant_issued` (a code or codeless offer the partner configured; also applies), `observed` (seen in the wild; trust `probability`). Lead with `generated` when you have one.
- **`partnered`** is true when Checkmate is integrated with the store and can mint a guaranteed code. Say "here is a code for you"; for other merchants say "this code has worked recently".
- **`cashback`** is what the shopper earns on any order placed through `activation_url`, as a percentage of the order. It is null when we have nothing reliable to offer.
- **`activation_url`** is the link that attributes the order so cashback is paid. Send the shopper through it whenever it is present, even with no code.
- **`estimated_shipping`**: median, first and third quartile in days, plus `basis`, which says where the numbers come from. `measured_delivery`: observed order-to-delivery times for this merchant. `processing_plus_transit_estimate`: the merchant’s observed order-to-dispatch time plus an assumed 2 days in transit. `network_default`: we have no order data for this merchant yet, so this is what most US stores in the network deliver in (3–5 days); phrase it as "typically", not as this merchant’s figure. Null for merchants where delivery does not apply — travel, food delivery, restaurants, telecom, finance, software, streaming, tickets — so an assistant is never handed a shipping estimate for a hotel booking.
- **History `type`** is `purchased` (an order we saw) or `visited` (a store the shopper browsed through Checkmate). Purchases come first.

## Endpoint reference

Generated from the OpenAPI document served at `/openapi.json`, so it cannot drift from what the API validates. Every non-2xx response has the body `{ "error": { "code", "message" } }`.

### POST /v1/merchants/search

**Batch merchant search** · operationId `searchMerchants`

Search for merchants by name (fuzzy) or domain (exact match). Up to 50 queries per request. Each query carries its own `query_id` that is echoed back so callers can correlate responses. Each result contains an `items` array with matched merchants. Name queries return up to `limit` items per result (default 5, max 20). Misses return a single item with `id: null` — there are no 404s.

Request body:

- `queries` (array of object, required) [1–50 items] — Batch of search queries (1–50). Each query returns a result with an `items` array. Name queries with `limit > 1` may yield multiple items. Misses return an `items` array with a single entry where `id` is null.
  - `query_id` (string, required) — Caller-supplied opaque identifier correlated back in results
  - `type` (`name` | `domain`, required) — "name" triggers fuzzy search; "domain" is an exact-match lookup
  - `value` (string, required) — Search term: merchant name fragment or bare domain
  - `region` (string, optional) — Optional ISO 3166-1 alpha-2 region filter (name queries only)
  - `limit` (integer, optional) [range 1–20; default 5] — Maximum number of results to return for this query (name queries only, domain queries always return at most one result). Defaults to 5.

Response `200` — Per-query results. Each result has an `items` array. A name query with limit > 1 may produce multiple items. Misses have id: null

- `results` (array of object, required)
  - `query_id` (string, required)
  - `items` (array of object, required) [1–∞ items] — Matched merchants for this query, best first. Name queries are ordered by exact prefix match, then by `popularity_rank`, so `items[0]` is the merchant to use unless the shopper said otherwise. Always contains at least one entry; misses return a single entry with `id: null`.
    - `id` (string | null, required) — Checkmate merchant ID, or null when not found
    - `name` (string | null, required) — Merchant display name, or null when no match
    - `domains` (array of string, required) — Merchant domains
    - `popularity_rank` (integer | null, required) — Rank by shopper traffic across the Checkmate network, 1 = most visited. When several merchants share a name (there are three "Adidas"), the one with the lowest rank is almost always the one the shopper means. Null for unranked merchants.
    - `estimated_shipping` (object | null, required) — Shipping time statistics. Present for every matched merchant that ships physical goods — check `basis` to see whether it is this merchant’s own data or the network default. Null for merchants where delivery does not apply (travel, food delivery, restaurants, telecom, finance, software, streaming, tickets) and on a miss (`id: null`).
      - `unit` (`days`, required) — Unit for all shipping time values in this object
      - `median` (number, required) — Median days from order to delivery
      - `q1` (number | null, required) — First quartile (25th percentile) days to deliver, or null when unknown
      - `q3` (number | null, required) — Third quartile (75th percentile) days to deliver, or null when unknown
      - `basis` (`measured_delivery` | `processing_plus_transit_estimate` | `network_default`, required) — Where the numbers come from. `measured_delivery`: observed order-to-delivery times for this merchant. `processing_plus_transit_estimate`: the merchant’s observed order-to-dispatch time plus an assumed 2 days in transit. `network_default`: we have no order data for this merchant yet, so this is what most US stores in the network deliver in (3–5 days); phrase it as "typically", not as this merchant’s figure.
      - `sample_size` (integer | null, required) — Orders the figures were computed from, or null when the source does not report it. Treat small samples with caution.

Response `400` — Invalid request body or query cap exceeded (max 50 queries)

Response `401` — Missing or invalid API key

Response `429` — Too many requests. Rate limits are determined on a per partner basis

### POST /v1/merchants/codes

**Batch merchant offer codes** · operationId `getMerchantCodes`

Retrieve ranked offer codes for a set of merchants. Every merchant in the network returns an `activation_url` whether or not it has a code today, so an empty `items` is not a miss — check `activation_url` for that. Up to 20 queries per request. Each result contains an `items` array of codes sorted best-first by `probability`, plus a `partnered` flag; misses return an empty `items` array. Use `min_probability` and `limit` to receive only codes worth showing a shopper. Add an `idempotency_key` to deduplicate requests; we suggest using `sha256(user_email + date)` or similar.

Request body:

- `queries` (array of object, required) [1–20 items] — Batch of codes queries (1–20)
  - `query_id` (string, required) — Caller-supplied opaque identifier correlated back in results
  - `merchant_id` (string, required) — Checkmate merchant ID obtained from the search endpoint
  - `idempotency_key` (string, required) — Deduplication key for this query. We suggest using `sha256(user_email + date)` or similar.
  - `min_probability` (number, optional) [range 0–1] — Drop codes whose `probability` is below this threshold. Omit to receive every code. A value around 0.5 keeps only codes that usually work at checkout.
  - `limit` (integer, optional) [range 1–20] — Maximum number of codes to return for this query, taken from the top of the ranked list. Omit to receive up to 20.

Response `200` — Per-query results. Each result has an `items` array of codes. Misses return an empty `items` array

- `results` (array of object, required)
  - `query_id` (string, required)
  - `cashback` (object | null, required) — Cashback a shopper earns on any order placed through `activation_url`, independent of whether a code is used. This is the offer for the many merchants that have no reliable code: "no code today, but 3% back through this link". Null when Checkmate does not offer cashback at this merchant. Codes and cashback stack.
    - `value_type` (`percent`, required)
    - `value_amount` (number, required) — Percent of the order value returned to the shopper as cashback
  - `partnered` (boolean, required) — True when the merchant has a direct Checkmate partnership, so codes at the top of `items` are issued by the merchant (probability 1) rather than observed in the wild. False for unknown merchants.
  - `activation_url` (string | null, required) — Attributed link to the merchant, set for every merchant in the network and independent of `items`. This is how an empty `items` is told apart from an unknown merchant: a non-null `activation_url` means we know the store and the order is still tracked, there is just no code worth showing. Null means the merchant_id matched nothing.
  - `suppressed_code_count` (number, required) — How many codes your `min_probability` or `limit` removed from `items`. A zero-length `items` with a non-zero count here means codes exist but none cleared your threshold — not that the merchant has nothing.
  - `items` (array of object, required) — Offer codes sorted best-first by `probability` (minted / static / codeless / partnership / public), so `items[0]` is the code to show. Empty array on miss.
    - `code` (string | null, required) — Promo code string, or null for codeless offers
    - `probability` (number, required) [range 0–1] — Probability (0–1) that the code works at checkout right now. Computed from recent checkout attempts with newer attempts weighted more heavily, and discounted for codes with fewer than two lifetime successes. Merchant-issued and freshly minted codes are 1. Rank and gate on this field. It will usually differ from `success_count / apply_count`, which is an all-time ratio.
    - `single_use` (boolean, required) — True when the code expires after one redemption
    - `source` (`generated` | `merchant_issued` | `observed`, required) — Where this code came from. `generated`: created at the store for this request — it did not exist before you asked, it is single-use, and it will apply at checkout. `merchant_issued`: a code or codeless offer the partnered merchant configured with Checkmate; it also applies. `observed`: a public code seen in the wild whose `probability` is its measured recent success rate. Only OpenStock returns the first kind; a coupon feed can only return the third.
    - `value_type` (`percent` | `fixed` | `free_shipping` | `unknown`, required) — How the discount value is expressed. Minted and campaign codes carry the merchant-configured value; public codes are parsed from their description and fall back to "unknown" when the copy does not state a value.
    - `value_amount` (number | null, required) — Numeric discount amount; semantics depend on value_type (e.g. 10 = 10% or $10)
    - `currency` (string | null, required) — ISO 4217 currency code, present when value_type is "fixed"
    - `conditions` (array of string, required) — Machine-readable condition tags (e.g. minimum_subtotal, product_restricted, customer_restricted, applies_on_subscription). "up_to" marks a percent value that is a ceiling ("up to 50% off"), not a guaranteed saving.
    - `redirect_url` (string | null, required) — Checkmate affiliate-attributed deep-link; null when unavailable
    - `expires_at` (number | null, required) — Unix timestamp (ms) when the code expires, or null when it does not expire
    - `last_success` (string | null, required) — ISO 8601 UTC timestamp of the most recent successful checkout with this code, or null when never observed
    - `description` (string | null, required) — Human-readable description of the offer or null
    - `apply_count` (number | null, required) — All-time number of checkout attempts with this code since we first saw it, or null when not tracked (campaign and minted codes). Lifetime volume, not recent reliability — use `probability` for that. A high count with a low `probability` is a code that used to work and has since been retired by the merchant.
    - `success_count` (number | null, required) — All-time number of those attempts that yielded a saving, or null when not tracked. Pair with `apply_count` for the lifetime ratio; pair `last_success` with `probability` for whether it works today.

Response `400` — Invalid request body, missing idempotency_key, or query cap exceeded (max 20)

Response `401` — Missing or invalid API key

Response `429` — Too many requests. Rate limits are determined on a per partner basis

### POST /v1/shoppers/history

**Batch shopper purchase history** · operationId `getShopperHistory`

Look up purchase history for one or more shoppers identified by SHA-256 hash of their normalised (lowercase-trimmed) email address. Never send raw email addresses. Up to 20 queries per request. Each result contains a flat `items` array of merchant interactions; unknown shoppers return an empty `items` array — there are no 404s.

Request body:

- `queries` (array of object, required) [1–20 items] — Batch of shopper-history queries (1–20)
  - `query_id` (string, required) — Caller-supplied opaque identifier correlated back in results
  - `email_sha256` (string, required) — SHA-256 of the lowercase, trimmed email address, as 64 hex characters in either case. Never send the raw email.

Response `200` — Per-query shopper history. Each result has a flat `items` array of merchant interactions. Unknown shoppers return an empty array

- `results` (array of object, required)
  - `query_id` (string, required)
  - `items` (array of object, required) — Flat list of merchant interactions for this shopper. Empty array when the shopper is unknown.
    - `merchant_id` (string, required) — Checkmate merchant ID
    - `merchant_name` (string, required)
    - `type` (`visited` | `purchased`, required) — "purchased" when an order was observed at the merchant; "visited" when only browsing sessions were observed. Purchased merchants are listed first.

Response `400` — Invalid request body, malformed email_sha256, or query cap exceeded (max 20)

Response `401` — Missing or invalid API key

Response `429` — Too many requests. Rate limits are determined on a per partner basis

### GET /health

**Health check** · operationId `getHealth`

Returns the current health status of the Openstock API.

Response `200` — Service is healthy

- `ok` (boolean, required) — Whether the service is healthy
- `environment` (string, required) — Deployment environment name

## Caveats, stated plainly

- A public code’s `probability` is a measured rate, not a guarantee; only minted codes are.
- Cashback is null for some merchants. That is a real answer, not missing data.
- Shopper history covers shoppers Checkmate has seen; a shopper outside the network returns an empty result. Only the SHA-256 of the email is ever sent or stored.
- Merchant search is a prefix match on names; a one-letter query returns many candidates, so prefer a domain when you have one.

## Links

- Full guide (this file): https://api.openstock.sh/llms-full.txt
- OpenAPI 3.1: https://api.openstock.sh/openapi.json
- Interactive reference: https://api.openstock.sh/docs
- Live demo: https://openstock.sh/demo
- Status: https://status.openstock.sh
- Site: https://openstock.sh
