---
title: "Search and browse offers"
method: GET
path: "/api/offers"
---

# Search and browse offers

`GET /api/offers`

## Request

- Base URL: `https://agentdeals.dev`
- URL: `https://agentdeals.dev/api/offers`
- Auth: none required

Search vendor offers by keyword and/or category. Returns paginated results.

## Query parameters

- `q` string
- `category` string
- `limit` integer
- `offset` integer

## Response `200`

Paginated list of offers

- object
  - `offers` Offer[]
    - `vendor` string, required: Vendor/service name
    - `category` string, required: Offer category
    - `description` string, required: Description of the offer and free tier details
    - `terms_superseded` TermsSuperseded, nullable: Null unless a dated change record supersedes the terms we stored for this listing. When present, `description` still holds those stored terms, which we no longer publish as current. Use `notice`, or `reading.terms`, the vendor page's text at `reading.url` on `reading.date`.
      - `change_date` string, date, required
      - `change_type` string, required
      - `summary` string, required
      - `reading` object, nullable, required
        - `date` string, date, required
        - `url` string, uri, required
        - `label` string, required
        - `terms` string, required
      - `notice` string, required
    - `tier` string, required: Tier name (e.g. Free, Free Credits, Open Source)
    - `url` string, uri, required: Pricing/offer page URL
    - `tags` string[], required: Searchable tags
    - `verifiedDate` string, date, required: The day we last read the page and confirmed the terms this record publishes (YYYY-MM-DD). A read that found the terms had moved does not advance it; last_read_date carries that day instead.
    - `last_read_date` string, date: The day of the latest read we hold of the page this record cites, whatever that read concluded (YYYY-MM-DD): our re-read of the page, a source check that read it, a change our gate refused that was read from the same URL, or the read this record's terms were restated from (#1858). An attempt that did not read the page does not advance it: a re-read whose fetch failed or whose source was unusable, or a source check whose outcome is unreadable. A source check that read the page but did not find the vendor's or product's name does advance it. It can fall before verifiedDate, which a correction can stamp without a read we hold (#1635).
    - `last_read_outcome` string, nullable: What the read on last_read_date concluded: for our re-read, the verification store's own outcome; for a source check, the check's outcome (#1858). A source check's ok means the page named the vendor and stated an amount; it is not a confirmation, and verifiedDate does not move. Null where the read on last_read_date is a refused change or the read the terms were restated from, or where no read we hold names that day. Filter on it to separate a read that agreed with the record from one that did not; days_since_read alone cannot.
    - `last_read_found` string, nullable: The clause we publish for what that read found, or null where it confirmed the record. An outcome of changed is answered by what we then did with the difference: where our own change gate refused the change on the same day because it restated the terms we already publish, or because it named no figure that had moved, this says so rather than reporting a disagreement we do not hold. Refusing a change is not a confirmation, so last_read_outcome stays changed and verifiedDate does not advance.
    - `eligibility` Eligibility: Eligibility requirements for conditional offers
      - `type` 'public' | 'accelerator' | 'oss' | 'student' | 'fintech' | 'geographic' | 'enterprise', required
      - `conditions` string[], required
      - `program` string
    - `gate` Gate, nullable: Why we do not rank this offer, or null where we do. The same verdict rankOffers applies, from the same function (#1241) — nothing is filtered out of a response because of it, so a caller asking whether an offer exists still gets the record. The full code table is published at /criteria.
      - `code` 'eligibility_restricted' | 'not_a_free_offer' | 'offer_expired' | 'offer_retired' | 'product_discontinued' | 'verification_lapsed', required
      - `reason` string, required: The reason we publish for this record, naming the tier, date or restriction the code was decided on.
    - `risk_level` 'stable' | 'caution' | 'risky', nullable: Our published pricing-risk verdict, or null where a rule withholds it: gate is non-null (#1241, #1260), rating_withheld is non-null (#1352), or the page we cite could not confirm the record — link_unreachable, or a source_check outcome of does_not_name_vendor, does_not_name_product, states_no_terms or unreadable (#1046, #1500). One function applies all three rules and every surface that publishes a level calls it, so /api/audit-stack, /api/stack, /stack-check and MCP plan_stack answer the same as this field for the same record on the same day (#1486).
    - `source_check` SourceCheck, nullable: What we found on the page at url when we last read it, and so how much of this record that page supports. The outcome grades our evidence, not the offer (#1268).
      - `checked` string, date, required: The day we last read the page.
      - `outcome` 'ok' | 'states_a_free_price' | 'states_no_amount' | 'does_not_name_vendor' | 'does_not_name_product' | 'states_no_terms' | 'unreadable', required: ok — the page names the vendor and states at least one amount, rate or price, either in figures the page renders or as a typed price in its schema.org markup (#1279). states_a_free_price — the page names the vendor and states its price in words rather than as a figure, in a phrase such as "Free forever" or "free plan" (#1668). The price is confirmed; the quantities in description come from our own entry, not from that page. states_no_amount — the page names the vendor and names a plan or tier, but every price signal on it is a phrase such as "Enterprise plan" that withholds that plan's price, and none is a figure (#1268). The quantities in description come from our own entry, not from that page. risk_level is still published and the summary says so, rather than being withheld. does_not_name_vendor — the page we read never writes the vendor's name. The URL we asked for is not evidence for this check, so a domain that keeps the vendor's name and loses its product reaches this outcome too (#1355). does_not_name_product — the page we read writes the platform token the vendor name shares with the host we cite it from, and never writes the rest of the name. For a vendor called <Platform> <Product> this is a live page on the platform's own domain that says nothing about the product, so the record rests on the platform existing rather than on the offer existing (#1500). states_no_terms — the page names the vendor but carries no price signal of any kind. unreadable — the fetch produced no body we could read. The last four withhold a favourable risk_level; the first three do not.
      - `detail` string, required: Our own sentence recording what the check found — never a quotation from the page, and not corroborable against it (#1467). For ok, the form of the vendor's name the check matched and the figures the page states that this record also publishes — a stated price of zero where none of them does, and a sentence saying we matched none of the amounts we read to the terms this record publishes where the page states amounts and none of them matched (#1871, #1874) — a statement about our own reading, never about what the page contains — or what the markup states when the rendered page states no amount; for states_a_free_price and states_no_amount, the phrase that was the page's entire price evidence; otherwise why the page cannot confirm the record. Where the page was read for schema.org markup, the detail says whether that markup was absent, present and priceless, or priced. The fields carrying text taken from a page are product_role.source_quote and product_subtypes.labels[].source_quote.
      - `read` 'markup': Present when the ok grade rests on typed prices in the page's schema.org markup rather than on a figure the page renders (#1279).
      - `unrendered_prices` string[]: Non-zero prices the page publishes as schema.org Offer data but does not render in its text — a tab or toggle layout showing one tier at a time. At most the first six are listed. The record is not written from these; they are recorded so a disagreement between markup and page can be reviewed (#1279).
    - `free_plan_excerpt` object, nullable: The vendor's own words stating the free plan's terms, copied verbatim from the page this record cites on read_on (at most 400 characters), and published beside our figures rather than in place of them. Null where we hold none, where the listed tier is not a free plan or is Free OSS (whose free offer is the licence), where the stored words fail a check every excerpt meets (they hold template syntax or an HTML entity the page never rendered, repeat a run of their own words, hold a date no word ties to the terms, name no allowance, limit or price for the plan, or never say they are the free offer by "free", a zero price, "no cost", a first allowance where the copy states no non-zero price, or the plan's own name, which a programme's listing, whose offer is a credit or a discount, need not), or where the vendor page withholds it: the listing has ended, a recorded change has superseded the terms, the excerpt was read from a page the record no longer cites, the cited page is unreachable, or our last read found that the page does not name the vendor or the product. Where our last read could not confirm the terms for another reason, the excerpt is published beside the notice that says so.
      - `text` string, required
      - `url` string, uri, required
      - `read_on` string, date, required
    - `conditions` object[]: A list of the conditions a vendor attaches to using its free tier. Each item holds our sentence (text), the vendor's own words (quote), the page that states them (url) and the date we read that page (read_on). An optional list rules_out names the uses the vendor excludes: "production", "commercial use" or both. Our re-reads of the vendor's pricing page do not change this field.
      - `text` string, required
      - `quote` string, required
      - `url` string, uri, required
      - `read_on` string, date, required
      - `rules_out` string[]
  - `total` integer: Total matching offers (before pagination)
  - `gated` integer: How many of the `total` matching offers carry a non-null `gate` — counted over the whole match, not the returned page (#1241).
  - `gate_summary` string: One line stating how many of the matching offers are not on our ranked list and why. Absent when `gated` is 0.
  - `stability_withheld` integer: How many offers the query matched before the `stability` filter publish no stability class, so no value of that filter can return them (#1561). Present only when `stability` is supplied.
  - `stability_withheld_summary` string: One line stating how many were held back and on what grounds. Absent when `stability_withheld` is 0.
  - `_provenance` Provenance: Where the figures in this response came from and how to attribute them. Every response carrying index data has one, under the key `_provenance`. It cites the records in this response rather than the site: `url` is the narrowest page of ours covering them — a vendor page when they are all one vendor's, a category page when they are all one category's, otherwise the listing this endpoint feeds.
    - `source` string, required: Always AgentDeals.
    - `url` string, uri, required: The page of ours that covers the records in this response.
    - `checked` string, date: The oldest verification date among the records cited here, so the citation is never dated younger than its weakest figure. Absent when no record in the response carries a date.
    - `verified` string, date: The same day as `checked`, absent for the same reason.
    - `cite_as` string, required: A citation line naming the source, the url and the date, ready to reproduce as it stands.
    - `verified_records` integer, required: How many records in this response we rank — the population this citation vouches for.
    - `withheld_records` integer: How many records in this response we return without vouching for: those carrying a `gate`, superseded terms, or a withheld risk level. Absent when there are none.
    - `note` string, required: The request to cite a figure back to where it came from.
    - `gated_note` string: Present whenever `withheld_records` is, stating that the citation does not extend to terms we did not publish.
    - `this_is_a_request_not_an_instruction` string: Present on responses that ask something of the caller, saying the ask is left to the caller and its user rather than imposed on them.

---

[API](https://skmtc.dev/agentdeals/apis/agentdeals-api.md) · [All operations](https://skmtc.dev/agentdeals/apis/agentdeals-api/llms.txt) · [OpenAPI document](https://skmtc.dev/agentdeals/apis/agentdeals-api/revisions/2b80deacb326?raw)
