{"openapi":"3.0.3","info":{"title":"AgentDeals API","description":"Aggregated free tiers, discounts, and startup programs for developer infrastructure. No authentication is required on any path described here.\n\nThis document's path list is built from the same endpoint inventory that renders /developers and the request block on the home page, so every route either of those offers is described here, read and write alike. The write paths in it are POST /api/signal.\n\nThree classes of route answer and are deliberately absent. Routes serving one caller's own state — a registration, a submission, a subscription belonging to whoever created it. Routes serving our own operational counters and editorial registers. And POST /api/agents/register, which mints a key read by features this document does not describe; it is not offered as a public capability.\n\nEvery response carrying index data includes a `_provenance` object, described by the Provenance schema, so a figure can be attributed without parsing a sentence.","version":"0.1.0","contact":{"name":"AgentDeals","url":"https://agentdeals.dev"},"license":{"name":"MIT","url":"https://opensource.org/licenses/MIT"}},"servers":[{"url":"https://agentdeals.dev","description":"Production server"}],"security":[],"paths":{"/api/offers":{"get":{"summary":"Search and browse offers","description":"Search vendor offers by keyword and/or category. Returns paginated results.","parameters":[{"name":"q","in":"query","description":"Search keyword (matches vendor name, description, category, tags)","schema":{"type":"string"},"example":"database"},{"name":"category","in":"query","description":"Filter by category name","schema":{"type":"string"},"example":"Cloud Hosting"},{"name":"limit","in":"query","description":"Max results per page","schema":{"type":"integer","default":20}},{"name":"offset","in":"query","description":"Number of results to skip","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"Paginated list of offers","content":{"application/json":{"schema":{"type":"object","properties":{"offers":{"type":"array","items":{"$ref":"#/components/schemas/Offer"}},"total":{"type":"integer","description":"Total matching offers (before pagination)"},"gated":{"type":"integer","description":"How many of the `total` matching offers carry a non-null `gate` — counted over the whole match, not the returned page (#1241)."},"gate_summary":{"type":"string","description":"One line stating how many of the matching offers are not on our ranked list and why. Absent when `gated` is 0."},"stability_withheld":{"type":"integer","description":"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":{"type":"string","description":"One line stating how many were held back and on what grounds. Absent when `stability_withheld` is 0."},"_provenance":{"$ref":"#/components/schemas/Provenance"}}},"example":{"offers":[{"vendor":"Supabase","category":"Cloud Hosting","description":"Open-source Firebase alternative with Postgres database, auth, storage, and edge functions. Free tier: 2 projects, 500MB database, 1GB file storage, 50K monthly active users.","tier":"Free","url":"https://supabase.com/pricing","tags":["database","auth","serverless"],"verifiedDate":"2026-03-01","gate":null}],"total":1,"gated":0}}}}}}},"/api/categories":{"get":{"summary":"List all categories","description":"Returns all offer categories with the number of offers in each, the scope statement that says what the name holds, and the other category names that answer the same question. Several names answer one question and hold disjoint sets, so a count alone does not say whether a category is the whole answer: read `also_answering` before deciding a category is empty of what you want. Names in `retired_names` are accepted by `category=` filters and redirect on `/category/`.","responses":{"200":{"description":"List of categories with counts, scope statements and the names that answer alongside them","content":{"application/json":{"schema":{"type":"object","properties":{"categories":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"slug":{"type":"string"},"count":{"type":"integer"},"audience":{"type":"string","enum":["developer","personal"],"description":"Who the products under this name are sold to."},"holds":{"type":"string","enum":["products","programmes"],"description":"`programmes` means every member is qualified-access — a startup or accelerator offer, not a free tier anyone can open."},"scope":{"type":"string","description":"One line saying what this name holds and what it does not."},"answers":{"type":"array","items":{"type":"string"},"description":"The question or questions this name answers."},"also_answering":{"type":"array","description":"Other categories answering one of the same questions. Their sets are disjoint from this one.","items":{"type":"object","properties":{"name":{"type":"string"},"slug":{"type":"string"},"count":{"type":"integer"}}}},"example_members":{"type":"array","items":{"type":"string"}}}}},"example_members_basis":{"type":"string"},"retired_names":{"type":"object","additionalProperties":{"type":"string"},"description":"Category names no longer published, mapped to the name that replaced them."},"_provenance":{"$ref":"#/components/schemas/Provenance"}}},"example":{"categories":[{"name":"Cloud Storage","slug":"cloud-storage","count":9,"audience":"personal","holds":"products","scope":"Consumer file-sync drives a person keeps documents and photos in — not storage an application writes to.","answers":["where do my files live"],"also_answering":[{"name":"CDN","slug":"cdn","count":19},{"name":"Storage","slug":"storage","count":55}],"example_members":["Google Drive","Google Photos","iCloud"]}],"retired_names":{"Startup Programs":"Startup Perks"}}}}}}}},"/api/new":{"get":{"summary":"Recently added or updated offers","description":"Returns offers where verifiedDate falls within the last N days.","parameters":[{"name":"days","in":"query","description":"Number of days to look back (1-30)","schema":{"type":"integer","default":7},"example":7},{"name":"limit","in":"query","description":"Max results to return","schema":{"type":"integer","default":50}}],"responses":{"200":{"description":"List of offers by catalogue date, newest first","content":{"application/json":{"schema":{"type":"object","properties":{"offers":{"type":"array","items":{"$ref":"#/components/schemas/Offer"}},"total":{"type":"integer"},"_provenance":{"$ref":"#/components/schemas/Provenance"}}}}}}}}},"/api/newest":{"get":{"summary":"Newest deals","description":"Returns deals sorted by catalogue date (newest first) with days_since_update. Use for periodic 'what's new' checks.","parameters":[{"name":"since","in":"query","description":"Only return deals whose catalogue date is on or after this date. Accepts a calendar date (YYYY-MM-DD) or an ISO-8601 timestamp, which is narrowed to the date it names — the whole of that day is returned either way. A date that does not exist answers 400. Default: 30 days ago","schema":{"type":"string","format":"date"}},{"name":"limit","in":"query","description":"Max results (default: 20, max: 50)","schema":{"type":"integer","default":20}},{"name":"category","in":"query","description":"Filter by category name","schema":{"type":"string"}}],"responses":{"200":{"description":"List of newest deals with days_since_update","content":{"application/json":{"schema":{"type":"object","properties":{"deals":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Offer"},{"type":"object","properties":{"days_since_update":{"type":"integer"}}}]}},"total":{"type":"integer"},"_provenance":{"$ref":"#/components/schemas/Provenance"}}}}}}}}},"/api/changes":{"get":{"summary":"Deal and pricing changes","description":"Returns tracked pricing and tier changes across vendors. Filter by date, change type, vendor, or category. A tracked pricing change is a change a vendor made to published terms, which we recorded, still stands, and we have not withdrawn. Most carry the date the terms took effect; the rest carry the date we read the page. The rule and the four totals it separates are at /changes#what-counts, and every response carries them as change_census.","parameters":[{"name":"since","in":"query","description":"Only return changes dated on or after this date. Accepts a calendar date (YYYY-MM-DD) or an ISO-8601 timestamp, which is narrowed to the date it names — the whole of that day is returned either way. A date that does not exist answers 400. Default: 30 days ago, and only when no other filter is passed — a request carrying type, vendor, vendors, categories or category searches the whole change log unless since says otherwise. The filter compares against each record's date. Where date_meaning is \"effective\", that is the day the change took effect; where it is \"discovered\", it is the day we recorded the change, and when it took effect is unknown. Every response states the window it applied under date_window.","schema":{"type":"string","format":"date"},"example":"2025-01-01"},{"name":"type","in":"query","description":"Filter by change type. Every type a record can carry is accepted; the same list types the `change_type` field on the records that come back.","schema":{"type":"string","enum":["free_tier_removed","open_source_killed","product_deprecated","limits_reduced","restriction","pricing_restructured","pricing_model_change","limits_increased","new_free_tier","new_tier","startup_program_expanded","pricing_postponed","rebranded","record_corrected"]}},{"name":"vendor","in":"query","description":"Filter by vendor name. Matched by case-insensitive substring, not by exact name: vendor=Pilot returns GitHub Copilot's records and category=ai returns Email's. Nothing is narrowed on your behalf, so read name_match, which every response carries: it names what this filter matched and whether each match was exact.","schema":{"type":"string"}},{"name":"vendors","in":"query","description":"Comma-separated vendor names to filter by (e.g. 'Vercel,Supabase,Clerk'). Matched by case-insensitive substring, not by exact name: vendor=Pilot returns GitHub Copilot's records and category=ai returns Email's. Nothing is narrowed on your behalf, so read name_match, which every response carries: it names what this filter matched and whether each match was exact.","schema":{"type":"string"},"example":"Vercel,Supabase"},{"name":"category","in":"query","description":"Comma-separated category names to filter by (e.g. 'Database,Cloud Hosting'). Matched by case-insensitive substring, not by exact name: vendor=Pilot returns GitHub Copilot's records and category=ai returns Email's. Nothing is narrowed on your behalf, so read name_match, which every response carries: it names what this filter matched and whether each match was exact.","schema":{"type":"string"},"example":"Database,Hosting"},{"name":"categories","in":"query","description":"Comma-separated category names to filter by (e.g. 'Database,Cloud Hosting'). Matched by case-insensitive substring, not by exact name: vendor=Pilot returns GitHub Copilot's records and category=ai returns Email's. Nothing is narrowed on your behalf, so read name_match, which every response carries: it names what this filter matched and whether each match was exact.","schema":{"type":"string"},"example":"Database,Hosting"},{"name":"limit","in":"query","description":"Max results per page","schema":{"type":"integer","default":20}},{"name":"offset","in":"query","description":"Number of results to skip","schema":{"type":"integer","default":0}},{"name":"include_retracted","in":"query","description":"Records we have withdrawn as our own error (standing 'retracted') are left out unless you ask for them. Set true to receive them alongside the rest; they arrive carrying standing 'retracted' and impact 'none'. Either way, retracted_excluded reports how many your query matched and did not receive. Anything other than 'true' or 'false' answers 400 rather than being ignored.","schema":{"type":"boolean","default":false}},{"name":"include_index_housekeeping","in":"query","description":"Records of our own index housekeeping (reports 'our_index') are left out unless you ask for them. They say we stopped listing an offer of ours, not that the vendor changed anything, so counting them as vendor activity overstates it. Set true to receive them alongside the rest. Either way, index_housekeeping_excluded reports how many your query matched and did not receive. Anything other than 'true' or 'false' answers 400 rather than being ignored.","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"List of deal changes","content":{"application/json":{"schema":{"type":"object","properties":{"changes":{"type":"array","items":{"$ref":"#/components/schemas/PublishedDealChange"}},"total":{"type":"integer","description":"Records matching your query before paging, counting only the ones served. Retracted records are outside this count unless include_retracted is true, and our own index housekeeping is outside it unless include_index_housekeeping is true. Measured over the same population as all_time_total, so a type-filtered total and the whole-log figure can be compared."},"returned":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"},"include_retracted":{"type":"boolean","description":"The value this response was built with, echoed back."},"retracted_excluded":{"type":"integer","description":"How many records matched your query and were withheld for carrying standing 'retracted'. Zero when include_retracted is true, because nothing was withheld. Read this before comparing a count against an earlier one."},"include_index_housekeeping":{"type":"boolean","description":"The value this response was built with, echoed back."},"index_housekeeping_excluded":{"type":"integer","description":"How many records matched your query and were withheld for carrying reports 'our_index'. Zero when include_index_housekeeping is true, because nothing was withheld. Read this before comparing a count against an earlier one."},"date_window":{"$ref":"#/components/schemas/ChangeDateWindow"},"advisory":{"type":"array","items":{"$ref":"#/components/schemas/PublishedDealChange"},"description":"Top 3 high-impact changes outside your filter, drawn from the last 30 days whatever date_window says — this block is what is worth knowing lately, so it stays recent even when your query searches the whole log. Never contains a retracted record, whatever include_retracted says."},"summary":{"type":"object","properties":{"stack_changes_count":{"type":"integer"},"ecosystem_high_impact_count":{"type":"integer"},"period_days":{"type":"integer"}}},"all_time_total":{"type":"integer","description":"The whole change log measured by the published rule, unaffected by your query. Equal to change_census.tracked_pricing_changes. A tracked pricing change is a change a vendor made to published terms, which we recorded, still stands, and we have not withdrawn. Most carry the date the terms took effect; the rest carry the date we read the page."},"change_census":{"type":"object","description":"Every total we publish is one of these four and names which. A tracked pricing change is a change a vendor made to published terms, which we recorded, still stands, and we have not withdrawn. Most carry the date the terms took effect; the rest carry the date we read the page. The rule is at /changes#what-counts.","properties":{"tracked_pricing_changes":{"type":"integer","description":"tracked pricing changes. A tracked pricing change is a change a vendor made to published terms, which we recorded, still stands, and we have not withdrawn. Most carry the date the terms took effect; the rest carry the date we read the page."},"changes_still_in_force":{"type":"integer","description":"changes still in force. Adds our own bookkeeping: corrections we made to our own records, each dated the day we made it, and index housekeeping — a source we dropped, written up as the vendor deprecating a product."},"records_we_stand_behind":{"type":"integer","description":"records we stand behind. Adds changes the vendor has since reversed. They happened; they no longer hold."},"records_held":{"type":"integer","description":"records in the change log. Adds records we withdrew as our own error. They describe events that did not happen."},"retrievable_from_this_door":{"type":"integer","description":"How many records this request could return before paging. Follows include_retracted and include_index_housekeeping, so it is the only figure here that your query moves."},"rule_url":{"type":"string","format":"uri"},"note":{"type":"string"}}},"_provenance":{"$ref":"#/components/schemas/Provenance"}}},"example":{"changes":[{"vendor":"Heroku","change_type":"free_tier_removed","date":"2022-11-28","summary":"Heroku eliminated all free dynos, free Postgres, and free Redis.","previous_state":"Free dyno (550-1000 hrs/mo), free Postgres (10K rows), free Redis (25MB)","current_state":"No free tier. Cheapest plan: $5/mo Eco dyno.","impact":"high","source_url":"https://blog.heroku.com/next-chapter","category":"Cloud Hosting","alternatives":["Railway","Render","Fly.io"]}],"total":1,"returned":1,"limit":20,"offset":0}}}},"400":{"description":"Invalid since parameter","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/details/{vendor}":{"get":{"summary":"Vendor detail with alternatives","description":"Get detailed information about a specific vendor's offer. Optionally includes alternatives in the same category. Accepts canonical vendor names (e.g. \"Supabase\") or short-form slugs (e.g. \"kiro\" → Amazon Kiro, \"proton\" → multi-product disambiguation). Fuzzy matches resolve in-place and return `resolved_from`, except where every record the input would reach is one we publish as ended — those return 404 naming the ended record rather than answering about it. Ambiguous inputs return 200 with a `disambiguation` array instead of an `offer`.","parameters":[{"name":"vendor","in":"path","required":true,"description":"Vendor name or short-form slug (URL-encoded)","schema":{"type":"string"},"example":"Supabase"},{"name":"alternatives","in":"query","description":"Include alternative vendors in the same category","schema":{"type":"string","enum":["true","false"],"default":"false"}}],"responses":{"200":{"description":"Vendor offer details, OR a disambiguation list when the input matches multiple vendors (e.g. \"proton\"). When the input was fuzzy-matched to a canonical vendor, the response includes `resolved_from`.","content":{"application/json":{"schema":{"allOf":[{"oneOf":[{"type":"object","description":"Resolved vendor offer. `relatedVendors`, `alternatives` and `tie_break` are siblings of `offer`, never fields on it, and each is returned once (#1777).","properties":{"offer":{"$ref":"#/components/schemas/Offer"},"relatedVendors":{"type":"array","items":{"type":"string"},"description":"Every alternative we hold in this vendor's category, named, in the order tie_break describes. Not a selection of them (#1774). Returned whether or not alternatives=true."},"alternatives":{"type":"array","items":{"$ref":"#/components/schemas/Offer"},"description":"Only present when alternatives=true. The same set relatedVendors names, in the same order, with each record in full."},"tie_break":{"$ref":"#/components/schemas/TieBreak"},"resolved_from":{"type":"string","description":"Original input when fuzzy-matched to a canonical vendor (e.g. input \"kiro\" resolves to Amazon Kiro). Absent when the input was already canonical."},"_agent":{"$ref":"#/components/schemas/AgentBlock"}}},{"type":"object","description":"Disambiguation list when input matches multiple vendors","required":["disambiguation","resolved_from"],"properties":{"disambiguation":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"}}}},"resolved_from":{"type":"string"}}}]},{"type":"object","properties":{"_provenance":{"$ref":"#/components/schemas/Provenance"}},"required":["_provenance"]}]}}}},"400":{"description":"Missing vendor name","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Vendor not found: either no exact match and no fuzzy resolution, or the only records the input reaches have ended, which we name in `suggestions` rather than substitute","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"suggestions":{"type":"array","items":{"type":"string"},"description":"Similar vendor names"}}}}}}}}},"/api/compare":{"get":{"summary":"Compare two vendors side by side","description":"Returns a structured comparison of two developer tool vendors including free tier details, pricing, and recent deal changes.","parameters":[{"name":"a","in":"query","required":true,"description":"First vendor name (case-insensitive; a qualified name such as 'Supabase database' resolves to the vendor it names, see VendorMatch)","schema":{"type":"string"},"example":"Supabase"},{"name":"b","in":"query","required":true,"description":"Second vendor name (case-insensitive; a qualified name such as 'Neon free tier' resolves to the vendor it names, see VendorMatch)","schema":{"type":"string"},"example":"Neon"}],"responses":{"200":{"description":"Side-by-side vendor comparison","content":{"application/json":{"schema":{"type":"object","properties":{"vendor_a":{"$ref":"#/components/schemas/Offer"},"vendor_b":{"$ref":"#/components/schemas/Offer"},"vendor_a_match":{"$ref":"#/components/schemas/VendorMatch"},"vendor_b_match":{"$ref":"#/components/schemas/VendorMatch"},"shared_categories":{"type":"boolean"},"category_overlap":{"type":"array","items":{"type":"string"}},"_provenance":{"$ref":"#/components/schemas/Provenance"}}}}}},"400":{"description":"Missing required parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"One or both vendors not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"suggestions_a":{"type":"array","items":{"type":"string"}},"suggestions_b":{"type":"array","items":{"type":"string"}}}}}}}}}},"/api/audit-stack":{"get":{"summary":"Audit your infrastructure stack","description":"Analyze your current services for pricing risks, cost savings, and missing capabilities. Returns per-service risk assessment, cheaper alternatives, and gap analysis.","parameters":[{"name":"services","in":"query","required":true,"description":"Comma-separated vendor names (e.g. 'Vercel,Supabase,Clerk'). An audit reports only names it matched exactly; anything else comes back as status not_found with suggestions, so that risks_found and cheaper_alternative are never computed for a product you did not name (#1269).","schema":{"type":"string"},"example":"Vercel,Supabase,Clerk"}],"responses":{"200":{"description":"Stack audit results","content":{"application/json":{"schema":{"type":"object","properties":{"services_analyzed":{"type":"number"},"risks_found":{"type":"number","description":"Services carrying a published caution or risky level. A service whose level is withheld is not counted here — it is unrated, not safe (#1486)."},"savings_opportunities":{"type":"number"},"gaps":{"type":"array","items":{"type":"object","properties":{"category":{"type":"string"},"recommendation":{"type":"object","properties":{"vendor":{"type":"string"},"tier":{"type":"string"},"description":{"type":"string"},"terms_superseded":{"$ref":"#/components/schemas/TermsSuperseded"},"conditions":{"type":"array","description":"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.","items":{"type":"object","properties":{"text":{"type":"string"},"quote":{"type":"string"},"url":{"type":"string","format":"uri"},"read_on":{"type":"string","format":"date"},"rules_out":{"type":"array","items":{"type":"string","enum":["production","commercial use"]}}},"required":["text","quote","url","read_on"]}}}}}}},"services":{"type":"array","items":{"type":"object","properties":{"vendor":{"type":"string"},"status":{"type":"string","enum":["found","not_found"]},"category":{"type":"string"},"tier":{"type":"string"},"risk_level":{"type":"string","enum":["stable","caution","risky"],"nullable":true,"description":"The same level /api/offers publishes for the record this name resolved to, from the same function (#1486) — null wherever any rule withholds it, and never substituted with a favourable default."},"risk_cause":{"type":"object","nullable":true,"description":"The single dated record that produced a non-stable risk_level."},"gate":{"type":"object","nullable":true,"description":"Non-null for an offer we have decided not to list (#1241). The caller named this vendor, so the gate is reported whether or not a level would have been published."},"rating_withheld":{"type":"object","nullable":true,"description":"Non-null where every record that would have set a non-stable risk_level either carries an empty source_url (no_source, #1352) or is a change our automatic re-read recorded that neither an archived copy of the vendor's page nor our own check has confirmed (unconfirmed, #1952). The level is withheld rather than reported as stable, and the records are named on the vendor page as unsourced or unconfirmed. Where both kinds are withheld, reason is that of the record that would set the most severe level, and unconfirmed on a tie; records counts both.","properties":{"reason":{"type":"string","enum":["no_source","unconfirmed"]},"records":{"type":"number"}}},"link_unreachable":{"type":"object","nullable":true,"description":"Non-null where the offer's own link is confirmed unreachable (#1046)."},"source_check":{"type":"object","nullable":true,"description":"Our last read of the page we cite. An outcome other than ok withholds a favourable level."},"level_withheld_because":{"type":"string","nullable":true,"description":"The sentence naming the rule that withheld the level, or null where a level is published. A null risk_level always arrives with one."},"recent_changes":{"type":"array","items":{"type":"object"}},"cheaper_alternative":{"type":"object","nullable":true,"description":"A same-category free offer we publish a stable level for. Chosen with the same function as risk_level, so a vendor we decline to rate is never offered as the safer swap (#1486)."},"suggestions":{"type":"array","items":{"type":"string"}}}}},"recommendations":{"type":"array","items":{"type":"string"}},"_provenance":{"$ref":"#/components/schemas/Provenance"}}}}}},"400":{"description":"Missing services parameter","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/vendor-risk/{vendor}":{"get":{"summary":"Check vendor pricing stability and risk","description":"Before depending on a vendor's free tier, check if their pricing is stable. Returns risk level (stable/caution/risky), pricing change history, free tier longevity, and more-stable alternatives.","parameters":[{"name":"vendor","in":"path","required":true,"description":"Vendor name (case-insensitive; a qualified name such as 'Vercel Pro plan' resolves to the vendor it names, see VendorMatch)","schema":{"type":"string"},"example":"Vercel"}],"responses":{"200":{"description":"Vendor risk assessment","content":{"application/json":{"schema":{"type":"object","properties":{"vendor":{"type":"string"},"vendor_match":{"$ref":"#/components/schemas/VendorMatch"},"category":{"type":"string"},"risk_level":{"type":"string","enum":["stable","caution","risky"],"nullable":true,"description":"Null where the offer's own link is confirmed unreachable (#1046) — we withhold the level rather than publish a favourable one we cannot check. Also null where gate is non-null (#1241): an offer we have decided not to list is not one we rate. Also null where rating_withheld is non-null (#1352, #1952): the only records that would rate this vendor cite no source or are changes no archived copy of its page has confirmed."},"link_unreachable":{"type":"object","nullable":true,"description":"Non-null only where a check reached a conclusion about the destination: a 404 confirmed by a full request, a 410, or a hostname with no address. Being refused (403, 429, 5xx, timeout) is evidence about our checker and never produces one of these.","properties":{"last_reachable":{"type":"string","format":"date","nullable":true},"checked":{"type":"string","format":"date"},"terminal":{"type":"boolean"}}},"risk_cause":{"type":"object","nullable":true,"description":"The single dated record that produced a non-stable risk_level. Never null when risk_level is caution or risky (#1038) — a client that renders the level must be able to render the reason.","properties":{"vendor":{"type":"string"},"date":{"type":"string"},"change_type":{"type":"string"},"summary":{"type":"string"},"source_url":{"type":"string","nullable":true,"description":"The page this record was read from, or null where we hold none. Carried on the cause so a client rendering the reason can cite it."},"current_state":{"type":"string"},"resolution":{"$ref":"#/components/schemas/ChangeResolution"},"archive_check":{"type":"object","nullable":true,"description":"Our check of this record against archived copies of the vendor's page (#1952). Absent where we have not checked the record. Only the outcome no_usable_capture changes how the record counts: it sets no caution or risky label, and where it is the only record that would, the vendor's rating is withheld (rating_withheld, reason unconfirmed) rather than reported as stable; a standing narrowing with it also withholds a favourable stability class; and our pages mark the record unconfirmed. Every other outcome leaves the record counting as it did before the check.","properties":{"checked":{"type":"string","format":"date","description":"The day we ran the check."},"outcome":{"type":"string","enum":["vendor_changed","ours","removal_stated_before","no_usable_capture","text_day_unknown","page_unreadable_today"],"description":"'vendor_changed' — archived copies show the vendor's page changing. 'ours' — the archived copy from when our listing text was written already states what the record calls new, so the difference was in our text, not on the vendor's page. 'removal_stated_before' — that archived copy already states the removal, so the removal predates our text. 'no_usable_capture' — no archived copy could settle the record either way. 'text_day_unknown' — we do not know when our listing text was written, so there was no capture to compare. 'page_unreadable_today' — we could not read the vendor's page when we ran the check, so no comparison ran."},"brackets":{"type":"array","description":"Where outcome is vendor_changed: each pair of archived copies between which the page changed, the last showing the old terms and the first showing the new. Where there is exactly one, date_meaning is effective_by: the change took effect after last_old and on or before date.","items":{"type":"object","properties":{"last_old":{"type":"string","format":"date","description":"The day of the last copy showing the old terms."},"first_new":{"type":"string","format":"date","nullable":true,"description":"The day of the first copy showing the new terms, or null where only our own read of the page shows them."},"last_old_capture":{"type":"string","format":"uri","nullable":true,"description":"That copy's address in the Internet Archive."},"first_new_capture":{"type":"string","format":"uri","nullable":true,"description":"That copy's address in the Internet Archive."}},"required":["last_old","first_new"]}},"capture_day":{"type":"string","format":"date","nullable":true,"description":"Where outcome is ours or removal_stated_before: the day of the archived copy from when our listing text was written."},"capture":{"type":"string","format":"uri","nullable":true,"description":"That copy's address in the Internet Archive."},"stated_then":{"type":"string","nullable":true,"description":"The line in that copy that settled the check, as the copy prints it: the terms the record called new (ours), or the removal (removal_stated_before)."},"recorded_as":{"type":"string","nullable":true,"enum":["free_tier_removed","open_source_killed","product_deprecated","limits_reduced","restriction","pricing_restructured","pricing_model_change","limits_increased","new_free_tier","new_tier","startup_program_expanded","pricing_postponed","rebranded","record_corrected"],"description":"Where outcome is ours: the change type our re-read recorded. The record's change_type is record_corrected."}},"required":["checked","outcome"]}}},"rating_withheld":{"type":"object","nullable":true,"description":"Non-null where every record that would have set a non-stable risk_level either carries an empty source_url (no_source, #1352) or is a change our automatic re-read recorded that neither an archived copy of the vendor's page nor our own check has confirmed (unconfirmed, #1952). The level is withheld rather than reported as stable, and the records are named on the vendor page as unsourced or unconfirmed. Where both kinds are withheld, reason is that of the record that would set the most severe level, and unconfirmed on a tie; records counts both.","properties":{"reason":{"type":"string","enum":["no_source","unconfirmed"]},"records":{"type":"number"}}},"gate":{"$ref":"#/components/schemas/Gate"},"source_check":{"$ref":"#/components/schemas/SourceCheck"},"free_tier_longevity_days":{"type":"number","nullable":true,"description":"Null where the gate code is offer_retired, product_discontinued or not_a_free_offer (#1241) — a count of days a free tier has held has no referent where our own record says there is no free tier."},"changes":{"type":"array","items":{"$ref":"#/components/schemas/DealChange"}},"tie_break":{"$ref":"#/components/schemas/TieBreak"},"alternatives":{"type":"array","description":"Every alternative we hold in this vendor's category, in the order tie_break describes. Not a selection of them (#1774).","items":{"type":"object","properties":{"vendor":{"type":"string"},"category":{"type":"string"},"tier":{"type":"string"},"risk_level":{"type":"string","enum":["stable","caution","risky"],"nullable":true,"description":"Null where this alternative's own link is confirmed unreachable (#1046), null where its rating_withheld is non-null (#1352), and null where its gate is non-null (#1241) — rankForListing demotes a gated record to the tail rather than dropping it, so an alternative can be one we do not list. The same function decides this as decides the level on the offer itself (#1486)."},"link_unreachable":{"type":"object","nullable":true,"properties":{"last_reachable":{"type":"string","format":"date","nullable":true},"checked":{"type":"string","format":"date"},"terminal":{"type":"boolean"}}},"gate":{"$ref":"#/components/schemas/Gate"},"risk_cause":{"type":"object","nullable":true,"description":"The single dated record that produced a non-stable risk_level. Never null when risk_level is caution or risky (#1038) — a client that renders the level must be able to render the reason.","properties":{"vendor":{"type":"string"},"date":{"type":"string"},"change_type":{"type":"string"},"summary":{"type":"string"},"source_url":{"type":"string","nullable":true,"description":"The page this record was read from, or null where we hold none. Carried on the cause so a client rendering the reason can cite it."},"current_state":{"type":"string"},"resolution":{"$ref":"#/components/schemas/ChangeResolution"},"archive_check":{"type":"object","nullable":true,"description":"Our check of this record against archived copies of the vendor's page (#1952). Absent where we have not checked the record. Only the outcome no_usable_capture changes how the record counts: it sets no caution or risky label, and where it is the only record that would, the vendor's rating is withheld (rating_withheld, reason unconfirmed) rather than reported as stable; a standing narrowing with it also withholds a favourable stability class; and our pages mark the record unconfirmed. Every other outcome leaves the record counting as it did before the check.","properties":{"checked":{"type":"string","format":"date","description":"The day we ran the check."},"outcome":{"type":"string","enum":["vendor_changed","ours","removal_stated_before","no_usable_capture","text_day_unknown","page_unreadable_today"],"description":"'vendor_changed' — archived copies show the vendor's page changing. 'ours' — the archived copy from when our listing text was written already states what the record calls new, so the difference was in our text, not on the vendor's page. 'removal_stated_before' — that archived copy already states the removal, so the removal predates our text. 'no_usable_capture' — no archived copy could settle the record either way. 'text_day_unknown' — we do not know when our listing text was written, so there was no capture to compare. 'page_unreadable_today' — we could not read the vendor's page when we ran the check, so no comparison ran."},"brackets":{"type":"array","description":"Where outcome is vendor_changed: each pair of archived copies between which the page changed, the last showing the old terms and the first showing the new. Where there is exactly one, date_meaning is effective_by: the change took effect after last_old and on or before date.","items":{"type":"object","properties":{"last_old":{"type":"string","format":"date","description":"The day of the last copy showing the old terms."},"first_new":{"type":"string","format":"date","nullable":true,"description":"The day of the first copy showing the new terms, or null where only our own read of the page shows them."},"last_old_capture":{"type":"string","format":"uri","nullable":true,"description":"That copy's address in the Internet Archive."},"first_new_capture":{"type":"string","format":"uri","nullable":true,"description":"That copy's address in the Internet Archive."}},"required":["last_old","first_new"]}},"capture_day":{"type":"string","format":"date","nullable":true,"description":"Where outcome is ours or removal_stated_before: the day of the archived copy from when our listing text was written."},"capture":{"type":"string","format":"uri","nullable":true,"description":"That copy's address in the Internet Archive."},"stated_then":{"type":"string","nullable":true,"description":"The line in that copy that settled the check, as the copy prints it: the terms the record called new (ours), or the removal (removal_stated_before)."},"recorded_as":{"type":"string","nullable":true,"enum":["free_tier_removed","open_source_killed","product_deprecated","limits_reduced","restriction","pricing_restructured","pricing_model_change","limits_increased","new_free_tier","new_tier","startup_program_expanded","pricing_postponed","rebranded","record_corrected"],"description":"Where outcome is ours: the change type our re-read recorded. The record's change_type is record_corrected."}},"required":["checked","outcome"]}}},"rating_withheld":{"type":"object","nullable":true,"description":"Non-null where every record that would have set a non-stable risk_level either carries an empty source_url (no_source, #1352) or is a change our automatic re-read recorded that neither an archived copy of the vendor's page nor our own check has confirmed (unconfirmed, #1952). The level is withheld rather than reported as stable, and the records are named on the vendor page as unsourced or unconfirmed. Where both kinds are withheld, reason is that of the record that would set the most severe level, and unconfirmed on a tie; records counts both.","properties":{"reason":{"type":"string","enum":["no_source","unconfirmed"]},"records":{"type":"number"}}}}}},"summary":{"type":"string"},"_provenance":{"$ref":"#/components/schemas/Provenance"}}}}}},"404":{"description":"Vendor not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"suggestions":{"type":"array","items":{"type":"string"}}}}}}}}}},"/api/deadlines":{"get":{"summary":"Future-dated changes and stated deadlines, with a countdown","description":"Two kinds of entry, soonest first, each with the number of days remaining. A change record: every tracked change whose date is still ahead of today, returned as the record. A stated deadline: a date later than today that a sentence names in a listing's description or conditions, or in the summary of a change record dated today or earlier that is still in force, where that text also uses a deadline word such as shut down, retire, discontinue, deprecate, until or through. A stated deadline carries that sentence as its summary, and a stated_in field, which a change record does not. There is at most one stated deadline per vendor and date, none within a day of a future-dated change record of the same vendor, and where a vendor has stated deadlines on two consecutive days only the later is returned. Use it to find the deadlines a stack is about to run into rather than the ones it has already passed.","parameters":[{"name":"type","in":"query","description":"Return only entries of this change type. A stated deadline from a listing has no change type, so none is returned when this is set; one from a change record is returned when that record has this type.","schema":{"type":"string","enum":["free_tier_removed","open_source_killed","product_deprecated","limits_reduced","restriction","pricing_restructured","pricing_model_change","limits_increased","new_free_tier","new_tier","startup_program_expanded","pricing_postponed","rebranded","record_corrected"]}}],"responses":{"200":{"description":"Future-dated changes and stated deadlines ordered by date","content":{"application/json":{"schema":{"type":"object","properties":{"deadlines":{"type":"array","items":{"oneOf":[{"allOf":[{"$ref":"#/components/schemas/DealChange"},{"type":"object","properties":{"countdown_days":{"type":"integer","description":"Whole days from today, UTC, to the date on the record."}}}]},{"type":"object","required":["vendor","category","date","summary","source_url","stated_in","countdown_days"],"properties":{"vendor":{"type":"string"},"category":{"type":"string"},"date":{"type":"string","format":"date","description":"The date the sentence names."},"summary":{"type":"string","description":"The sentence that names the date, as our listing or change record writes it."},"source_url":{"type":"string","format":"uri","nullable":true,"description":"Where that sentence's text comes from: the link of the listing condition that states it (the listing's own URL where the condition has none), the listing's URL for its description, or the source_url of the change record whose summary states it. Null when that text has none."},"stated_in":{"type":"string","enum":["listing","record"],"description":"listing: a listing's description or one of its conditions states the date. record: the summary of a change record dated today or earlier states it."},"change_type":{"type":"string","enum":["free_tier_removed","open_source_killed","product_deprecated","limits_reduced","restriction","pricing_restructured","pricing_model_change","limits_increased","new_free_tier","new_tier","startup_program_expanded","pricing_postponed","rebranded","record_corrected"],"description":"Present only when stated_in is record: the type of the change record whose summary states the date. Absent when stated_in is listing."},"record_date":{"type":"string","format":"date","description":"Present only when stated_in is record: the date of the change record whose summary states the date."},"countdown_days":{"type":"integer","description":"Whole days from today, UTC, to the date the sentence names."}}}]}},"count":{"type":"integer"},"_provenance":{"$ref":"#/components/schemas/Provenance"}}}}}}}}},"/api/ai-coding-pricing":{"get":{"summary":"AI coding tool pricing","description":"The AI Coding records and the tracked changes against them, in one response. Every record is the same one /api/offers returns, with the path to our page for that vendor added.","parameters":[{"name":"type","in":"query","description":"Return only tools of this kind.","schema":{"type":"string","enum":["ide","cli","cloud-agent","app-builder"]}}],"responses":{"200":{"description":"AI coding tools, the changes recorded against them, and the kinds on offer","content":{"application/json":{"schema":{"type":"object","properties":{"tools":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Offer"},{"type":"object","properties":{"vendor_page":{"type":"string","description":"Path to our page for this vendor."}}}]}},"changes":{"type":"array","items":{"$ref":"#/components/schemas/DealChange"}},"count":{"type":"integer"},"categories":{"type":"array","items":{"type":"string"},"description":"The values the type parameter accepts."},"_provenance":{"$ref":"#/components/schemas/Provenance"}}}}}}}}},"/api/hosting-pricing":{"get":{"summary":"Cloud hosting and PaaS pricing","description":"The hosting and platform records and the tracked changes against them, in one response. has_referral says whether we hold a referral code for the vendor; the code itself is on /api/referral-codes/{vendor}.","parameters":[{"name":"type","in":"query","description":"Return only platforms of this kind.","schema":{"type":"string","enum":["traditional-paas","edge-serverless","full-featured","static-specialized"]}}],"responses":{"200":{"description":"Hosting platforms, the changes recorded against them, and the kinds on offer","content":{"application/json":{"schema":{"type":"object","properties":{"platforms":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Offer"},{"type":"object","properties":{"vendor_page":{"type":"string"},"has_referral":{"type":"boolean"}}}]}},"changes":{"type":"array","items":{"$ref":"#/components/schemas/DealChange"}},"count":{"type":"integer"},"categories":{"type":"array","items":{"type":"string"},"description":"The values the type parameter accepts."},"_provenance":{"$ref":"#/components/schemas/Provenance"}}}}}}}}},"/api/llm-pricing":{"get":{"summary":"LLM API pricing","description":"The LLM API records and the tracked changes against them, in one response. A provider with no free allowance is still returned, with its gate stating why we do not rank it.","parameters":[{"name":"type","in":"query","description":"Return only providers of this kind.","schema":{"type":"string","enum":["frontier","inference","open-source-host","specialized"]}}],"responses":{"200":{"description":"LLM API providers, the changes recorded against them, and the kinds on offer","content":{"application/json":{"schema":{"type":"object","properties":{"providers":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Offer"},{"type":"object","properties":{"vendor_page":{"type":"string"}}}]}},"changes":{"type":"array","items":{"$ref":"#/components/schemas/DealChange"}},"count":{"type":"integer"},"categories":{"type":"array","items":{"type":"string"},"description":"The values the type parameter accepts."},"_provenance":{"$ref":"#/components/schemas/Provenance"}}}}}}}}},"/api/startup-credits":{"get":{"summary":"Startup credits and programmes","description":"The qualified-access records — accelerator, incubator and founder programmes — and the tracked changes against them. Every member has conditions on it: read eligibility before presenting a ceiling as available.","parameters":[{"name":"type","in":"query","description":"Return only programmes of this kind.","schema":{"type":"string","enum":["cloud-infrastructure","fintech-banking","developer-tools","ai-tools"]}}],"responses":{"200":{"description":"Startup programmes, the changes recorded against them, and the kinds on offer","content":{"application/json":{"schema":{"type":"object","properties":{"programs":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Offer"},{"type":"object","properties":{"vendor_page":{"type":"string"}}}]}},"changes":{"type":"array","items":{"$ref":"#/components/schemas/DealChange"}},"count":{"type":"integer"},"categories":{"type":"array","items":{"type":"string"},"description":"The values the type parameter accepts."},"_provenance":{"$ref":"#/components/schemas/Provenance"}}}}}}}}},"/api/referral-programs":{"get":{"summary":"Vendors running a referral or affiliate programme","description":"Vendors whose own referral programme is open to anyone, with what the referrer and the referee each get. A programme is listed once we have read its terms on the vendor's page, and not while that page is unreachable or redirects to another page. This is not the list of codes we hold and earn on — that is /api/referral-codes, and /disclosure names every one of them.","parameters":[{"name":"category","in":"query","description":"Filter by vendor category name. The categories field lists the ones present.","schema":{"type":"string"},"example":"Cloud Hosting"}],"responses":{"200":{"description":"Referral programmes and the categories they fall in","content":{"application/json":{"schema":{"type":"object","properties":{"programs":{"type":"array","items":{"type":"object","properties":{"vendor":{"type":"string"},"category":{"type":"string"},"referrer_benefit":{"type":"string","description":"What the person sharing the link gets."},"referee_benefit":{"type":"string","description":"What the person using the link gets."},"program_url":{"type":"string","format":"uri","description":"The vendor's own page describing the programme."},"type":{"type":"string"},"commission_type":{"type":"string"},"read_on":{"type":"string","format":"date","description":"The date we last read the programme's terms on the vendor's page."},"vendor_page":{"type":"string"}}}},"count":{"type":"integer"},"categories":{"type":"array","items":{"type":"string"}},"_provenance":{"$ref":"#/components/schemas/Provenance"}}}}}}}}},"/api/expiring":{"get":{"summary":"Get expiring deals","description":"Check which developer tool deals, free tiers, or credits are expiring soon. Returns deals sorted by expiration date (soonest first).","parameters":[{"name":"within_days","in":"query","description":"Number of days to look ahead (default: 30, max: 365)","schema":{"type":"integer","default":30,"minimum":1,"maximum":365}}],"responses":{"200":{"description":"Expiring deals","content":{"application/json":{"schema":{"type":"object","properties":{"deals":{"type":"array","items":{"type":"object","properties":{"vendor":{"type":"string"},"category":{"type":"string"},"description":{"type":"string"},"terms_superseded":{"$ref":"#/components/schemas/TermsSuperseded"},"tier":{"type":"string"},"url":{"type":"string","format":"uri"},"expires_date":{"type":"string","format":"date"},"days_until_expiry":{"type":"integer"}}}},"total":{"type":"integer"},"_provenance":{"$ref":"#/components/schemas/Provenance"}}}}}}}}},"/api/freshness":{"get":{"summary":"Get data freshness metrics","description":"Returns data quality metrics. Two counts are reported side by side and they are not interchangeable: confirmed_* counts offers whose published terms the verification store can source to a read that confirmed them, and stamped_* counts offers by the catalogue date beside the entry, which records when it was last written and is not a confirmation. freshness_score is computed from the confirmed count; the catalogue-date share is published separately as stamp_score.","parameters":[],"responses":{"200":{"description":"Data freshness metrics","content":{"application/json":{"schema":{"type":"object","properties":{"total_offers":{"type":"integer"},"stamped_within_7_days":{"type":"integer","description":"Offers whose catalogue date (verifiedDate) falls within 7 days. The catalogue date records when the entry was last written and is not a confirmation."},"stamped_within_30_days":{"type":"integer","description":"Offers whose catalogue date falls within 30 days."},"stamped_within_90_days":{"type":"integer","description":"Offers whose catalogue date falls within 90 days. This count was published as verified_within_90_days until 2026-09-14."},"stamped_within_180_days":{"type":"integer","description":"Offers whose catalogue date falls within 180 days."},"confirmed_within_7_days":{"type":"integer","description":"Offers whose published terms the verification store can source to a read that confirmed them within 7 days."},"confirmed_within_30_days":{"type":"integer","description":"Offers confirmed by a read within 30 days."},"confirmed_within_90_days":{"type":"integer","description":"Offers confirmed by a read within 90 days. freshness_score is computed from this."},"confirmed_within_180_days":{"type":"integer","description":"Offers confirmed by a read within 180 days."},"offers_holding_a_confirmation":{"type":"integer","description":"Offers for which the verification store holds any confirmation at all."},"oldest_confirmation_held":{"type":"string","format":"date","nullable":true,"description":"The oldest confirmation still held for any offer. This is not the date the store opened — a confirmation superseded by a later read is no longer held."},"attempted_within_90_days":{"type":"integer","description":"Offers the verification store attempted to re-read within 90 days, whether or not the read confirmed anything. Read against confirmed_within_90_days this separates offers we have not reached from offers we reached without confirming."},"freshness_score":{"type":"integer","description":"confirmed_within_90_days as a percentage of total_offers — the share of the catalogue whose published terms we can source to a read that confirmed them. This field reported the catalogue-date share until 2026-09-14; that share is now published as stamp_score."},"stamp_score":{"type":"integer","description":"stamped_within_90_days as a percentage of total_offers."},"stalest_entries":{"type":"array","items":{"type":"object","properties":{"vendor":{"type":"string"},"category":{"type":"string"},"verifiedDate":{"type":"string","format":"date"},"last_read_date":{"type":"string","format":"date"},"confirmed_on":{"type":"string","format":"date","nullable":true,"description":"The date the store confirmed these terms, or null where it holds none."},"url":{"type":"string","format":"uri"},"days_since_verified":{"type":"integer"},"days_since_read":{"type":"integer"}}}},"freshest_entries":{"type":"array","items":{"type":"object","properties":{"vendor":{"type":"string"},"category":{"type":"string"},"verifiedDate":{"type":"string","format":"date"},"last_read_date":{"type":"string","format":"date"},"confirmed_on":{"type":"string","format":"date","nullable":true,"description":"The date the store confirmed these terms, or null where it holds none."},"url":{"type":"string","format":"uri"},"days_since_verified":{"type":"integer"},"days_since_read":{"type":"integer"}}}},"by_category":{"type":"array","items":{"type":"object","properties":{"category":{"type":"string"},"count":{"type":"integer"},"avg_days_since_verified":{"type":"integer","description":"Mean age in days of the category's catalogue dates. Not a confirmation age."},"freshness_score":{"type":"integer","description":"confirmed_within_90_days as a percentage of the category's count."},"stamp_score":{"type":"integer","description":"stamped_within_90_days as a percentage of the category's count."},"confirmed_within_90_days":{"type":"integer"},"stamped_within_90_days":{"type":"integer"}}}},"quarantine":{"type":"object","description":"Offers the re-verification job has stopped checking daily because three consecutive checks failed. They are retried on a backoff; the entry says what failed and when it is next due.","properties":{"count":{"type":"integer"},"retry_after_days":{"type":"integer"},"by_reason":{"type":"object","additionalProperties":{"type":"integer"}},"entries":{"type":"array","items":{"type":"object","properties":{"vendor":{"type":"string"},"url":{"type":"string","format":"uri"},"consecutive_failures":{"type":"integer"},"failure_category":{"type":"string","nullable":true},"last_error":{"type":"string","nullable":true},"last_attempt_at":{"type":"string","format":"date","nullable":true},"last_success":{"type":"string","format":"date","nullable":true},"quarantined_since":{"type":"string","format":"date","nullable":true},"next_retry":{"type":"string","format":"date","nullable":true}}}}}},"superseded_terms":{"type":"object","description":"Offers whose stored description one of our own in-force change records names as the previous terms. Neither freshness_score nor stamp_score counts them: a read that finds a change records what it found in the change log and leaves the catalogue entry saying the old thing, so the entry is neither confirmed nor restated. The vendor page for such an offer withholds our stored terms and quotes the reading instead.","properties":{"offers_whose_stored_terms_a_record_supersedes":{"type":"integer","description":"Offers our own in-force change log contradicts."},"offers_we_could_restate_from_a_sourced_reading":{"type":"integer","description":"How many of those records carry a dated reading with a source URL — the sentence the vendor page already shows in place of our terms."},"oldest_reading_we_are_withholding_behind":{"type":"string","format":"date","nullable":true,"description":"The oldest such reading. How far back the longest-standing withheld entry goes."},"offers_we_may_restate_from_their_reading":{"type":"integer","description":"Withheld offers where the reading answers for the tier the entry describes, so it may be stored as our terms, attributed to the reading and to the date it was read."},"offers_we_refuse_to_restate":{"type":"object","additionalProperties":{"type":"integer"},"description":"The rest, counted by the ground we refuse on. A reading that prices a different plan than the entry describes is not a restatement of that entry. Nor is one that reports on the page it was read from rather than stating that page's terms, one that states no figure where the terms it would replace state several, or one that drops the cap on how many people may use the free tier — a restatement may drop a caveat, but not the fact that decides the tier. One ground is not a rule and says so in its name: it counts the readings held back by name, each of which is waiting on a rule that should reach it. One counts entries whose restatement was put back by hand: the record it was taken from may not restate it a second time, though a newer record contradicting the entry still may."},"offers_re_read_since_the_record_and_still_withheld":{"type":"integer","description":"Withheld offers whose cited page we have read again since the reading and which we still cannot answer from our own stored terms."},"restatements_keeping_the_stored_sentence_saying_what_the_product_is":{"type":"integer","description":"How many of the restatements we may make open on the stored clause saying what the product is, with only the terms after it replaced. The rest hold no such clause and take the reading whole."},"offers_whose_terms_came_from_a_reading":{"type":"integer","description":"Offers whose stored description was taken from a dated reading of the vendor's page rather than written by us or confirmed against that page. This is a fourth state beside confirmed, stamped and read — a read disagreed with us and we took its words — and it is counted here and nowhere else, because neither verifiedDate nor the confirmation date may move on a restatement."},"offers_restated_by_change_type":{"type":"object","additionalProperties":{"type":"integer"},"description":"Those offers counted by the verdict of the change record the reading came from."},"oldest_reading_we_publish_as_our_terms":{"type":"string","format":"date","nullable":true,"description":"The oldest reading standing as an offer's terms. How far back the longest-standing restatement goes."},"newest_reading_we_publish_as_our_terms":{"type":"string","format":"date","nullable":true,"description":"The newest such reading."}}}}}}}}}}},"/api/digest":{"get":{"summary":"Get weekly pricing digest","description":"Curated weekly summary of developer tool pricing changes, new offers, and upcoming deadlines. Falls back to 30-day window if fewer than 3 changes in the past week.","parameters":[],"responses":{"200":{"description":"Weekly digest","content":{"application/json":{"schema":{"type":"object","properties":{"week":{"type":"string","description":"Week date range"},"date_range":{"type":"string","description":"ISO date range"},"deal_changes":{"type":"array","items":{"type":"object","properties":{"vendor":{"type":"string"},"change_type":{"type":"string"},"date":{"type":"string","format":"date","description":"The day the change took effect if date_meaning is \"effective\". If date_meaning is \"discovered\", the day we recorded the change; its effective date is unknown. If date_meaning is \"effective_by\", the latest day the change can have taken effect. Where archived copies of the vendor's page bracket a change, it is dated by the first copy that shows the new terms."},"date_source":{"type":"string","enum":["vendor_page","hand_written","discovered"],"description":"Where date came from. \"vendor_page\": the vendor's page stated it. \"hand_written\": a person recorded it before this field existed. \"discovered\": the page stated no effective date, so date is the day we read the page — do not present it as the date the vendor changed anything."},"summary":{"type":"string"},"impact":{"type":"string","enum":["high","medium","low"]},"ends_a_free_tier":{"type":"boolean"},"listing_effect":{"type":"string","enum":["ends","narrows","none"]}}}},"new_offers":{"type":"array","items":{"type":"object","properties":{"vendor":{"type":"string"},"category":{"type":"string"},"description":{"type":"string"},"terms_superseded":{"$ref":"#/components/schemas/TermsSuperseded"},"conditions":{"type":"array","description":"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.","items":{"type":"object","properties":{"text":{"type":"string"},"quote":{"type":"string"},"url":{"type":"string","format":"uri"},"read_on":{"type":"string","format":"date"},"rules_out":{"type":"array","items":{"type":"string","enum":["production","commercial use"]}}},"required":["text","quote","url","read_on"]}}}}},"upcoming_deadlines":{"type":"array","items":{"type":"object","properties":{"vendor":{"type":"string"},"expires_date":{"type":"string","format":"date"},"days_until_expiry":{"type":"integer"}}}},"summary":{"type":"string","description":"One-paragraph summary of the week"},"_provenance":{"$ref":"#/components/schemas/Provenance"}}}}}}}}},"/api/digest/weekly":{"get":{"summary":"Weekly digest, formatted","description":"One week of tracked changes, rendered. format=json returns the digest as data with the markdown and HTML renderings alongside it; format=markdown and format=html return that rendering as the body, so only format=json carries a citation block.","parameters":[{"name":"format","in":"query","description":"Which rendering to return as the body. Default json.","schema":{"type":"string","enum":["json","markdown","html"],"default":"json"}},{"name":"limit","in":"query","description":"Max changes to include.","schema":{"type":"integer"}},{"name":"weeks_ago","in":"query","description":"0 is the current week, 1 the week before it.","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"The week's digest in the requested rendering","content":{"application/json":{"schema":{"type":"object","properties":{"week_of":{"type":"string","format":"date"},"week_ending":{"type":"string","format":"date"},"total_changes":{"type":"integer","description":"Every change we hold, not only this week's."},"changes_in_week":{"type":"integer","description":"Changes the vendor made within the week."},"discovered_in_week":{"type":"integer","description":"Changes recorded within the week, whatever date they carry. A change dated earlier that we found this week is counted here and not in changes_in_week."},"summary":{"type":"object","additionalProperties":{"type":"integer"},"description":"Counts by change type for the week."},"headline":{"type":"string"},"top_changes":{"type":"array","items":{"$ref":"#/components/schemas/DealChange"}},"discovered_changes":{"type":"array","items":{"$ref":"#/components/schemas/DealChange"}},"discovery_note":{"type":"string","description":"States which of the two counts the listed changes are drawn from."},"digest_markdown":{"type":"string"},"digest_html":{"type":"string"},"_provenance":{"$ref":"#/components/schemas/Provenance"}}}},"text/markdown":{"schema":{"type":"string"}},"text/html":{"schema":{"type":"string"}}}}}}},"/api/stack":{"get":{"summary":"Get free-tier stack candidates per role","description":"For each role in your project type (hosting, database, auth, …) returns the set of free-tier offers whose terms we can stand behind today — deliberately not a single pick, because under every signal we record dozens of them tie. Each candidate carries its demerits, with the recorded fact and date behind each, plus any recorded changes we disclose but do not rank on. This endpoint does NOT model technical fit between a product and a role; the caller must apply that. The method is published at /criteria, and every role returns the tie_break seed so its order can be recomputed independently.","parameters":[{"name":"use_case","in":"query","required":true,"description":"What you're building (e.g., 'Next.js SaaS app', 'API backend', 'static blog')","schema":{"type":"string"},"example":"Next.js SaaS app"},{"name":"requirements","in":"query","description":"Comma-separated infrastructure needs (e.g., 'database,auth,email')","schema":{"type":"string"},"example":"database,auth,email"}],"responses":{"200":{"description":"Candidate sets per role, with the evidence behind each","content":{"application/json":{"schema":{"type":"object","properties":{"use_case":{"type":"string"},"stack":{"type":"array","items":{"type":"object","properties":{"role":{"type":"string"},"category":{"type":"string"},"candidates":{"type":"array","description":"The rotated tied set for this role, capped at 8. Not ordered by merit — every member is indistinguishable under our criteria.","items":{"type":"object","properties":{"vendor":{"type":"string"},"tier":{"type":"string"},"description":{"type":"string"},"terms_superseded":{"$ref":"#/components/schemas/TermsSuperseded"},"conditions":{"type":"array","description":"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.","items":{"type":"object","properties":{"text":{"type":"string"},"quote":{"type":"string"},"url":{"type":"string","format":"uri"},"read_on":{"type":"string","format":"date"},"rules_out":{"type":"array","items":{"type":"string","enum":["production","commercial use"]}}},"required":["text","quote","url","read_on"]}},"url":{"type":"string","format":"uri"},"verified_date":{"type":"string","format":"date"},"last_read_date":{"type":"string","format":"date","description":"The day of the latest read we hold of this vendor's cited page, whatever that read concluded: our re-read, a source check that read the page, a change our gate refused that was read from the same URL, or the read its terms were restated from (#1858). It can fall before verified_date, because verified_date is a stamp the record carries rather than a confirmation the store can always source (#1635)."},"risk_level":{"type":"string","enum":["stable","caution","risky"],"nullable":true,"description":"The same level /api/offers publishes for this record, from the same function (#1486). Null where we withhold it rather than publish a favourable one we cannot stand behind. A null always arrives with the field naming the rule that withheld it, and with level_withheld_because."},"rating_withheld":{"type":"object","nullable":true,"description":"Non-null where every record that would have set a non-stable risk_level either carries an empty source_url (no_source, #1352) or is a change our automatic re-read recorded that neither an archived copy of the vendor's page nor our own check has confirmed (unconfirmed, #1952). The level is withheld rather than reported as stable, and the records are named on the vendor page as unsourced or unconfirmed. Where both kinds are withheld, reason is that of the record that would set the most severe level, and unconfirmed on a tie; records counts both.","properties":{"reason":{"type":"string","enum":["no_source","unconfirmed"]},"records":{"type":"number"}}},"gate":{"type":"object","nullable":true,"description":"Non-null for an offer we have decided not to list (#1241). A candidate set is drawn from ungated offers, so this is null in practice and is carried because the level's rules are one set."},"source_check":{"type":"object","nullable":true,"description":"Our last read of the page we cite. An outcome other than ok withholds a favourable level."},"level_withheld_because":{"type":"string","nullable":true,"description":"The sentence naming the rule that withheld the level, or null where a level is published."},"stability":{"type":"string","enum":["stable","watch","volatile","improving"],"nullable":true,"description":"Null where a favourable class would rest on records we cannot stand behind (#1561). Withheld by every reason that withholds risk_level: an unreachable pricing page, a page stating no amount, tier or rate we can read, a refused read, a narrowing that cites no source or that no archived copy of the vendor's page has confirmed, or a gated listing. A null always arrives with stability_withheld_because."},"stability_withheld":{"type":"object","nullable":true,"description":"Non-null where a standing narrowing for this vendor cites no source (no_source) or is a change our automatic re-read recorded that neither an archived copy of the vendor's page nor our own check has confirmed (unconfirmed, #1952), so a favourable stability class is withheld rather than published. Where both kinds stand, reason is unconfirmed; records counts both.","properties":{"reason":{"type":"string","enum":["no_source","unconfirmed"]},"records":{"type":"number"}}},"stability_withheld_because":{"type":"string","nullable":true,"description":"The code naming the rule that withheld the stability class, or null where a class is published."},"link_unreachable":{"type":"object","nullable":true,"description":"Non-null where the offer's own link has not resolved for us (#1046).","properties":{"last_reachable":{"type":"string","nullable":true},"checked":{"type":"string"},"terminal":{"type":"boolean"}}},"demerits":{"type":"array","description":"Empty when we hold nothing against the offer.","items":{"type":"object","properties":{"code":{"type":"string"},"points":{"type":"integer"},"reason":{"type":"string"},"date":{"type":"string"},"about_us":{"type":"boolean","description":"True when the demerit describes a limit of ours rather than a fact about the vendor."}}}},"disclosures":{"type":"array","description":"Recorded changes shown but never ranked on.","items":{"type":"object","properties":{"code":{"type":"string"},"date":{"type":"string"},"summary":{"type":"string"}}}}}}},"tie_count":{"type":"integer","description":"How many offers tie at zero demerits in this role."},"eligible_count":{"type":"integer"},"demoted_count":{"type":"integer"},"excluded_count":{"type":"integer"},"reason":{"type":"string"},"tie_break":{"$ref":"#/components/schemas/TieBreak"}}}},"method":{"type":"object","properties":{"policy":{"type":"string"},"not_modelled":{"type":"string"},"criteria_url":{"type":"string"}}},"total_monthly_cost":{"type":"string"},"limitations":{"type":"array","items":{"type":"string"}},"upgrade_path":{"type":"string"},"_provenance":{"$ref":"#/components/schemas/Provenance"}}}}}},"400":{"description":"Missing use_case parameter","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/costs":{"get":{"summary":"Check free tier coverage per service","description":"Free tier coverage only, not paid usage.","parameters":[{"name":"services","in":"query","required":true,"description":"Comma-separated list of vendor names","schema":{"type":"string"},"example":"Vercel,Supabase,Clerk"},{"name":"scale","in":"query","description":"Usage scale tier","schema":{"type":"string","enum":["hobby","startup","growth"],"default":"hobby"}}],"responses":{"200":{"description":"Cost estimates per service and total","content":{"application/json":{"schema":{"type":"object","properties":{"scale":{"type":"string","description":"Scale tier used for estimation"},"services":{"type":"array","items":{"type":"object","properties":{"vendor":{"type":"string"},"current_tier":{"type":"string"},"free_tier_limits":{"type":"string"},"conditions":{"type":"array","description":"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.","items":{"type":"object","properties":{"text":{"type":"string"},"quote":{"type":"string"},"url":{"type":"string","format":"uri"},"read_on":{"type":"string","format":"date"},"rules_out":{"type":"array","items":{"type":"string","enum":["production","commercial use"]}}},"required":["text","quote","url","read_on"]}},"estimated_monthly_cost":{"type":"string"},"free_alternative":{"type":"object","properties":{"vendor":{"type":"string"},"tier":{"type":"string"},"description":{"type":"string"},"conditions":{"type":"array","description":"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.","items":{"type":"object","properties":{"text":{"type":"string"},"quote":{"type":"string"},"url":{"type":"string","format":"uri"},"read_on":{"type":"string","format":"date"},"rules_out":{"type":"array","items":{"type":"string","enum":["production","commercial use"]}}},"required":["text","quote","url","read_on"]}}}},"recent_changes":{"type":"array","items":{"type":"string"}}}}},"total_estimated_cost":{"type":"string"},"savings_available":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}},"_provenance":{"$ref":"#/components/schemas/Provenance"}}}}}},"400":{"description":"Missing services parameter or invalid scale"}}}},"/api/query-log":{"get":{"summary":"Recent request log","description":"Returns recent request-level log entries for both MCP tool calls and REST API hits. Stored in Redis, capped at 1000 entries. Query parameter values are not published: each entry reports the parameter names it carried and the length of each value. Session identifiers are not published either; entries from one session share a session_index that is assigned within a single response and carries no meaning across responses.","parameters":[{"name":"limit","in":"query","description":"Number of entries to return (1-200)","schema":{"type":"integer","default":50}}],"responses":{"200":{"description":"Recent request log entries","content":{"application/json":{"schema":{"type":"object","properties":{"entries":{"type":"array","items":{"type":"object","properties":{"ts":{"type":"string","format":"date-time"},"type":{"type":"string","enum":["mcp","api","session_connect"]},"endpoint":{"type":"string"},"param_lengths":{"type":"object","description":"Parameter name to the character length of the value that was sent. Values themselves are retained internally and never published.","additionalProperties":{"type":"integer"}},"user_agent":{"type":"string"},"result_count":{"type":"integer"},"session_index":{"type":"integer","description":"Groups entries from the same session within this response only."},"client_info":{"type":"object","properties":{"name":{"type":"string"},"version":{"type":"string"}}}}}},"count":{"type":"integer"}}}}}}}}},"/api/pageviews":{"get":{"summary":"Page view analytics","description":"Returns server-side page view counts for today, yesterday, all-time top pages, and referrer breakdown. Bot traffic is excluded.","responses":{"200":{"description":"Page view data","content":{"application/json":{"schema":{"type":"object","properties":{"today":{"type":"object","properties":{"total":{"type":"integer"},"top_pages":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"views":{"type":"integer"}}}}}},"yesterday":{"type":"object","properties":{"total":{"type":"integer"},"top_pages":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"views":{"type":"integer"}}}}}},"all_time":{"type":"object","properties":{"total":{"type":"integer"},"top_pages":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"views":{"type":"integer"}}}}}},"referrers_today":{"type":"object","additionalProperties":{"type":"integer"}}}}}}}}}},"/api/traffic":{"get":{"summary":"Traffic by client class","description":"Our own request counters, split by what made the request — AI agent, crawler or browser — with MCP tool calls alongside web hits so the two can be compared. These are counts of visits to us, not index data, so no citation block is attached. available is false when the store backing the daily series is not configured, and error then says so.","responses":{"200":{"description":"Traffic counters over several windows","content":{"application/json":{"schema":{"type":"object","properties":{"today":{"$ref":"#/components/schemas/TrafficWindow"},"last_7d":{"$ref":"#/components/schemas/TrafficWindow"},"last_30d":{"$ref":"#/components/schemas/TrafficWindow"},"web_vs_mcp":{"type":"object","additionalProperties":{"type":"object"},"description":"Web hits, AI-agent hits and MCP tool calls per window, with the ratios between them. A ratio is null when the denominator is zero."},"available":{"type":"boolean","description":"Whether the daily series has a store behind it. False means only the since-boot counters are meaningful."},"error":{"type":"string","description":"Why the daily series is unavailable, when it is."},"since_boot_by_class":{"type":"object","additionalProperties":{"type":"integer"}},"since_boot_not_found":{"type":"integer"},"since_boot_redirects":{"type":"integer"},"not_found_sample":{"type":"array","items":{"type":"object","properties":{"ts":{"type":"string","format":"date-time"},"client_class":{"type":"string"},"status":{"type":"integer"},"path":{"type":"string"}}}},"sessions":{"type":"object","properties":{"daily":{"type":"array","items":{"type":"object"}},"today":{"type":"integer"},"recording_since":{"type":"string","format":"date","nullable":true,"description":"The first day of the daily series. A count before this date is not missing, it was never recorded."},"all_time":{"type":"integer"},"retention_days":{"type":"integer"}}},"ai_agent_trigger_families":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"description":"Which user agents we read as user-initiated, as search indexing, and as training crawls."},"notes":{"type":"array","items":{"type":"string"},"description":"What each figure does and does not count, including how much of it is our own crawling."},"storage":{"type":"object","description":"Whether the counters are being persisted, and the failures if not."},"vendor_series":{"type":"object","description":"State of the per-vendor daily series."}}}}}}}}},"/api/stats":{"get":{"summary":"Service statistics","description":"Returns connection and usage statistics. Session and usage counts persist across deploys via Redis.","responses":{"200":{"description":"Service statistics","content":{"application/json":{"schema":{"type":"object","properties":{"activeSessions":{"type":"integer","description":"Currently connected MCP sessions"},"totalSessionsAllTime":{"type":"integer","description":"Cumulative sessions across all deploys (persisted in Redis)"},"totalApiHitsAllTime":{"type":"integer","description":"Cumulative REST API hits across all deploys (persisted in Redis)"},"totalToolCallsAllTime":{"type":"integer","description":"Cumulative MCP tool calls across all deploys (persisted in Redis)"},"sessionsToday":{"type":"integer","description":"Sessions opened since midnight UTC. Persisted per day, so a deploy does not reset it. Counts only from the date the daily series began — see sessions.recording_since on /api/traffic."},"serverStarted":{"type":"string","format":"date-time","description":"ISO timestamp of current server start"},"clients":{"type":"object","additionalProperties":{"type":"integer"},"description":"Cumulative session counts per MCP client name (e.g. claude-desktop, cursor)"},"toolCallsByClient":{"type":"object","additionalProperties":{"type":"integer"},"description":"Cumulative MCP tool-call counts per MCP client name. Missing/empty client IDs bucket to 'unknown'. Values sum to totalToolCallsAllTime (invariant)."},"toolCallsByName":{"type":"object","additionalProperties":{"type":"integer"},"description":"Cumulative MCP tool-call counts per tool name. The tools we offer are search_deals, plan_stack, compare_vendors, track_changes, get_referral_code; counts against names no longer offered are historical and stay in the total. Pre-feature historical calls bucket to 'unknown'. Values sum to totalToolCallsAllTime (invariant)."}}}}}}}}},"/api/feed":{"get":{"summary":"Atom feed of pricing changes","description":"The same Atom body served at /feed.xml, which is the href every page's feed autodiscovery link carries.","responses":{"200":{"description":"Atom XML feed","content":{"application/atom+xml":{"schema":{"type":"string","format":"binary"}}}}}}},"/api/openapi.json":{"get":{"summary":"This document","description":"The OpenAPI 3.0 description of this API. Its path list is built from the same endpoint inventory that renders /developers and the request block on the home page, so a route reachable from either is described here.","responses":{"200":{"description":"OpenAPI 3.0 document","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/docs":{"get":{"summary":"Browsable API reference","description":"Renders this document as a page you can issue requests from.","responses":{"200":{"description":"HTML page","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/api/signal":{"post":{"summary":"Report a vendor you recommended","description":"The attribution beacon. Tell us which vendor from one of our responses you named in your answer, and whether your user went on to sign up. No authentication. What we record is the vendor slug, the event, and an optional note — notes may be shared verbatim with the vendor named, so send nothing about your user. Counts collected here are published as self-reported and never affect any order we publish. A caller that cannot issue a POST may send the same fields as a query string on a GET to this path with ack=1, which is described at /signal.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"event":{"type":"string","enum":["recommended","converted"],"description":"recommended when you named the vendor in an answer; converted when your user signed up. An unrecognised value is still recorded, and the response says it was not recognised."},"vendor":{"type":"string","description":"The vendor slug, as a response of ours carries it. A name we cannot resolve is recorded unresolved rather than refused."},"source":{"type":"string"},"agent":{"type":"string"},"agent_id":{"type":"string"},"note":{"type":"string","description":"Optional, and may be shared verbatim with the vendor named."}},"required":["event","vendor"]},"example":{"event":"recommended","vendor":"supabase"}},"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"event":{"type":"string"},"vendor":{"type":"string"},"note":{"type":"string"}}}}}},"responses":{"202":{"description":"Recorded","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"recorded":{"type":"string","description":"The event as we stored it."},"vendor":{"type":"string","nullable":true},"vendor_resolved":{"type":"boolean","description":"Present and false when the name did not resolve to a vendor we hold."},"unresolved_name":{"type":"string","description":"Present with vendor_resolved, carrying the name as sent."},"event_recognized":{"type":"boolean","description":"Present and false when the event is not one we know."},"valid_events":{"type":"array","items":{"type":"string"}},"note_received":{"type":"boolean"},"note_redacted":{"type":"boolean"},"note_published":{"type":"boolean"},"agent_id_reserved":{"type":"boolean"},"self_reported":{"type":"boolean","description":"Always true. Every count here is the caller's own claim and is published as one."},"affects_ranking":{"type":"boolean","description":"Always false."},"docs":{"type":"string"}}}}}},"400":{"description":"event or vendor is missing","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"error":{"type":"string"},"hint":{"type":"string"},"valid_events":{"type":"array","items":{"type":"string"}}}}}}},"405":{"description":"A method other than POST, or a GET without ack=1","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"error":{"type":"string"},"docs":{"type":"string"}}}}}},"413":{"description":"A body over 4096 bytes","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"error":{"type":"string"},"max_bytes":{"type":"integer"},"docs":{"type":"string"}}}}}},"429":{"description":"Over 60 requests a minute from one caller. Retry-After carries the wait.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"error":{"type":"string"},"retry_after_seconds":{"type":"integer"},"docs":{"type":"string"}}}}}}}}},"/api/referral-codes":{"get":{"summary":"List every referral code we hold","description":"Returns every active referral code AgentDeals holds, with the reader benefit and every restriction on each. No authentication required. Filter by vendor category. Individual vendor lookup: GET /api/referral-codes/{vendor}. Agent-submitted codes are retired: source=agent answers 200 with an empty list and the reason.","parameters":[{"name":"source","in":"query","description":"platform returns the codes we hold, which is every code we serve. agent is retired and returns an empty list with the reason.","schema":{"type":"string","enum":["platform","agent"]}},{"name":"category","in":"query","description":"Filter by vendor category slug (e.g. cloud-hosting). See /api/categories for valid slugs.","schema":{"type":"string"},"example":"cloud-hosting"}],"responses":{"200":{"description":"Active referral codes across all vendors","content":{"application/json":{"schema":{"type":"object","properties":{"codes":{"type":"array","items":{"$ref":"#/components/schemas/ReferralCodeListing"}},"total":{"type":"integer"},"_provenance":{"$ref":"#/components/schemas/Provenance"}},"required":["codes","total"]},"example":{"codes":[{"vendor":"Railway","category":"Cloud Hosting","code":"7RZL9q","referral_url":"https://railway.com?referralCode=7RZL9q","referee_benefit":"$20 in credits","restrictions":[],"source":"platform"}],"total":1}}}},"400":{"description":"Invalid source or unknown category slug","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/referral-codes/{vendor}":{"get":{"summary":"Get best referral code for a vendor","description":"Returns the referral code AgentDeals holds for a vendor, or 404 when we hold none. Same shape is inlined on /api/offers, /api/compare, /api/details/{vendor}, /api/newest, and MCP tool responses.","parameters":[{"name":"vendor","in":"path","required":true,"description":"Vendor name (case-insensitive)","schema":{"type":"string"},"example":"Railway"}],"responses":{"200":{"description":"Best available referral code","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ReferralCodeListing"},{"type":"object","properties":{"_provenance":{"$ref":"#/components/schemas/Provenance"}},"required":["_provenance"]}]},"example":{"vendor":"Railway","code":"7RZL9q","referral_url":"https://railway.com?referralCode=7RZL9q","referee_benefit":"$20 in credits","restrictions":[],"source":"platform"}}}},"404":{"description":"No active referral codes for the requested vendor","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/feed.xml":{"get":{"summary":"Atom feed of pricing changes","description":"Atom feed of developer tool pricing changes. Subscribe in any feed reader to stay updated on free tier removals, limit changes, and new deals. Also available at /api/feed.","responses":{"200":{"description":"Atom XML feed","content":{"application/atom+xml":{"schema":{"type":"string","format":"binary"}}}}}}}},"components":{"schemas":{"Provenance":{"type":"object","description":"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.","properties":{"source":{"type":"string","description":"Always AgentDeals."},"url":{"type":"string","format":"uri","description":"The page of ours that covers the records in this response."},"checked":{"type":"string","format":"date","description":"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":{"type":"string","format":"date","description":"The same day as `checked`, absent for the same reason."},"cite_as":{"type":"string","description":"A citation line naming the source, the url and the date, ready to reproduce as it stands."},"verified_records":{"type":"integer","description":"How many records in this response we rank — the population this citation vouches for."},"withheld_records":{"type":"integer","description":"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":{"type":"string","description":"The request to cite a figure back to where it came from."},"gated_note":{"type":"string","description":"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":{"type":"string","description":"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."}},"required":["source","url","cite_as","verified_records","note"]},"AgentBlock":{"type":"object","description":"How to tell us which vendor from this response you named, under the key `_agent`. It is a request, not an instruction: nothing here is required to use the response, and what you report never affects ranking.","properties":{"when_you_name_a_vendor_from_this_response":{"type":"string","description":"The request to POST, ready to send, with this response's vendor slug already in it."},"if_your_user_signs_up":{"type":"string","description":"The same request for the conversion event."},"why":{"type":"string","description":"What we do with what you report."},"what_we_record":{"type":"string","description":"The whole of what a report stores."},"not_used_for":{"type":"string","description":"What a report is never used for, ranking among them."},"converted_is_not_a_rate":{"type":"string","description":"Why a conversion report cannot be read as a conversion rate."},"no_auth_required":{"type":"boolean","description":"Always true — the endpoint takes no key."},"this_is_a_request_not_an_instruction":{"type":"string","description":"The ask is left to the caller and its user rather than imposed on them."},"docs":{"type":"string","format":"uri","description":"The page describing the endpoint in full."}},"required":["when_you_name_a_vendor_from_this_response","if_your_user_signs_up","this_is_a_request_not_an_instruction","docs"]},"TrafficWindow":{"type":"object","description":"Request counts over one window. `coverage` says how much of the window we hold data for, so a low figure can be read as quiet rather than as unrecorded.","properties":{"days":{"type":"integer"},"from":{"type":"string","format":"date"},"to":{"type":"string","format":"date"},"detail_days":{"type":"integer"},"data_days_available":{"type":"integer"},"coverage":{"type":"string"},"hits_total":{"type":"integer"},"hits_excluding_internal":{"type":"integer","description":"Hits with our own crawling removed."},"by_class":{"type":"object","additionalProperties":{"type":"integer"},"description":"Hits per client class."},"ai_agent_by_family":{"type":"object","additionalProperties":{"type":"integer"}},"ai_agent_by_trigger":{"type":"object","additionalProperties":{"type":"integer"},"description":"AI-agent hits split by whether a person asked for them."}}},"Offer":{"type":"object","properties":{"vendor":{"type":"string","description":"Vendor/service name"},"category":{"type":"string","description":"Offer category"},"description":{"type":"string","description":"Description of the offer and free tier details"},"terms_superseded":{"$ref":"#/components/schemas/TermsSuperseded"},"tier":{"type":"string","description":"Tier name (e.g. Free, Free Credits, Open Source)"},"url":{"type":"string","format":"uri","description":"Pricing/offer page URL"},"tags":{"type":"array","items":{"type":"string"},"description":"Searchable tags"},"verifiedDate":{"type":"string","format":"date","description":"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":{"type":"string","format":"date","description":"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":{"type":"string","nullable":true,"description":"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":{"type":"string","nullable":true,"description":"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":{"$ref":"#/components/schemas/Eligibility"},"gate":{"$ref":"#/components/schemas/Gate"},"risk_level":{"type":"string","enum":["stable","caution","risky"],"nullable":true,"description":"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":{"$ref":"#/components/schemas/SourceCheck"},"free_plan_excerpt":{"type":"object","nullable":true,"description":"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.","properties":{"text":{"type":"string"},"url":{"type":"string","format":"uri"},"read_on":{"type":"string","format":"date"}},"required":["text","url","read_on"]},"conditions":{"type":"array","description":"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.","items":{"type":"object","properties":{"text":{"type":"string"},"quote":{"type":"string"},"url":{"type":"string","format":"uri"},"read_on":{"type":"string","format":"date"},"rules_out":{"type":"array","items":{"type":"string","enum":["production","commercial use"]}}},"required":["text","quote","url","read_on"]}}},"required":["vendor","category","description","tier","url","tags","verifiedDate"]},"TermsSuperseded":{"type":"object","nullable":true,"description":"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`.","properties":{"change_date":{"type":"string","format":"date"},"change_type":{"type":"string"},"summary":{"type":"string"},"reading":{"type":"object","nullable":true,"properties":{"date":{"type":"string","format":"date"},"url":{"type":"string","format":"uri"},"label":{"type":"string"},"terms":{"type":"string"}},"required":["date","url","label","terms"]},"notice":{"type":"string"}},"required":["change_date","change_type","summary","reading","notice"]},"TieBreak":{"type":"object","description":"How this response ordered the entries it names, and how many there are. The order is a permutation seeded only on the UTC date and the query key, so anyone can recompute it. `tie_count` is how many entries carry no recorded demerit and so are indistinguishable under every signal we hold; `ranked_total` is how many entries the seed ordered in all. A door that names alternatives names `ranked_total` of them, never a prefix (#1774). `/api/details/{vendor}` and `/api/vendor-risk/{vendor}` both return this block at the top level of the response and name the same alternatives as each other, but they rank under different query keys — `related:<category>:<vendor>` and `vendor-risk-alternatives:<vendor>` — so for most vendors the two orders differ. `query_key` says which ranking produced the order you are reading.","properties":{"date":{"type":"string","format":"date"},"query_key":{"type":"string"},"seed":{"type":"string"},"tie_count":{"type":"integer"},"ranked_total":{"type":"integer","description":"Every entry the seed ordered, tied and demoted and gated together. The array this block describes carries all of them."},"algorithm":{"type":"string"}},"required":["date","query_key","seed","tie_count","ranked_total","algorithm"]},"VendorMatch":{"type":"object","description":"Which name you asked about and which record answered. A name we hold verbatim matches exact. A name we do not hold matches inferred only when a vendor name we do hold appears in it as a whole word or dotted segment — \"AWS Lambda Free\" resolves to AWS, \"launchdarkly.com\" to LaunchDarkly. A fragment that merely appears inside a longer vendor name resolves to nothing and returns 404 with suggestions, because an accidental hit used to be returned shaped exactly like an exact one (#1269).","properties":{"requested":{"type":"string","description":"The name as you sent it."},"matched":{"type":"string","description":"The vendor this response is about."},"type":{"type":"string","enum":["exact","inferred"]}},"required":["requested","matched","type"]},"SourceCheck":{"type":"object","nullable":true,"description":"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).","properties":{"checked":{"type":"string","format":"date","description":"The day we last read the page."},"outcome":{"type":"string","enum":["ok","states_a_free_price","states_no_amount","does_not_name_vendor","does_not_name_product","states_no_terms","unreadable"],"description":"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":{"type":"string","description":"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":{"type":"string","enum":["markup"],"description":"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":{"type":"array","items":{"type":"string"},"description":"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)."}},"required":["checked","outcome","detail"]},"Gate":{"type":"object","nullable":true,"description":"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.","properties":{"code":{"type":"string","enum":["eligibility_restricted","not_a_free_offer","offer_expired","offer_retired","product_discontinued","verification_lapsed"]},"reason":{"type":"string","description":"The reason we publish for this record, naming the tier, date or restriction the code was decided on."}},"required":["code","reason"]},"Eligibility":{"type":"object","description":"Eligibility requirements for conditional offers","properties":{"type":{"type":"string","enum":["public","accelerator","oss","student","fintech","geographic","enterprise"]},"conditions":{"type":"array","items":{"type":"string"}},"program":{"type":"string"}},"required":["type","conditions"]},"ReferralCodeListing":{"type":"object","properties":{"vendor":{"type":"string","description":"Vendor/service name"},"category":{"type":"string","nullable":true,"description":"Primary category for the vendor (null if unknown)"},"code":{"type":"string","description":"The referral code string"},"referral_url":{"type":"string","format":"uri","description":"Full referral URL the referee should visit"},"referee_benefit":{"type":"string","description":"What the person using the code gets (e.g. '$20 in credits')"},"restrictions":{"type":"array","items":{"type":"string"},"description":"Conditions the referee must meet before the benefit is theirs (e.g. 'A payment method must be linked'). Empty when the record records none. Publish these wherever you publish referee_benefit."},"source":{"type":"string","enum":["platform"],"description":"Always platform: a code AgentDeals holds and earns on. Agent-submitted codes are retired and none is served."}},"required":["vendor","code","referral_url","referee_benefit","restrictions","source"]},"DealChange":{"type":"object","properties":{"vendor":{"type":"string"},"change_type":{"type":"string","enum":["free_tier_removed","open_source_killed","product_deprecated","limits_reduced","restriction","pricing_restructured","pricing_model_change","limits_increased","new_free_tier","new_tier","startup_program_expanded","pricing_postponed","rebranded","record_corrected"]},"reports":{"type":"string","enum":["vendor_offer","our_index"],"description":"What the record is about. 'vendor_offer' — the vendor changed its own offer, and a vendor page is where that can be read. 'our_index' — we changed what we list while the vendor's offer stood still, so no vendor page evidences it and the record carries no source_url. Absent means vendor_offer. Filter these out of any figure you present as vendor market activity."},"date":{"type":"string","format":"date"},"summary":{"type":"string"},"previous_state":{"type":"string"},"current_state":{"type":"string"},"tier_direction":{"type":"string","enum":["narrowed","unchanged","widened"],"description":"Whether the tier we list is worse, the same, or better under current_state than it was under previous_state, judged with both in hand rather than read off change_type. Where it says the tier did not get worse we publish the record and take no risk verdict from it. Absent means we have not judged this record."},"impact":{"type":"string","enum":["high","medium","low"]},"source_url":{"type":"string","format":"uri"},"category":{"type":"string"},"alternatives":{"type":"array","items":{"type":"string"}},"resolution":{"$ref":"#/components/schemas/ChangeResolution"}},"required":["vendor","change_type","date","summary","previous_state","current_state","impact","source_url","category","alternatives"]},"PublishedDealChange":{"allOf":[{"$ref":"#/components/schemas/DealChange"},{"type":"object","properties":{"standing":{"type":"string","enum":["in_force","reversed","retracted"],"description":"Where the record stands with us. 'in_force' — the change happened and still holds. 'reversed' — it happened and has since been undone, so the record is true history. 'retracted' — the record was our error and we do not stand behind it. Read this rather than testing whether resolution is present: 'reversed' and 'retracted' are different answers to a reader asking whether to trust the record, and the presence of resolution flattens them into one."},"impact":{"type":"string","enum":["high","medium","low","none"],"description":"'none' whenever standing is 'retracted' — a record we have withdrawn describes no event, so it weighs nothing. Derived at the point of serving; the stored value is left alone."},"date_meaning":{"type":"string","enum":["effective","discovered","effective_by"],"description":"What this record's date means. \"effective\": the day the change took effect. \"discovered\": the day we recorded the change; when it took effect is unknown. \"effective_by\": the latest day the change can have taken effect. Where archived copies of the vendor's page bracket a change, it is dated by the first copy that shows the new terms. Read this field, not date_source: date_source says who wrote the record, not what its date means."},"ends_a_free_tier":{"type":"boolean"},"listing_effect":{"type":"string","enum":["ends","narrows","none"]},"archive_check":{"type":"object","nullable":true,"description":"Our check of this record against archived copies of the vendor's page (#1952). Absent where we have not checked the record. Only the outcome no_usable_capture changes how the record counts: it sets no caution or risky label, and where it is the only record that would, the vendor's rating is withheld (rating_withheld, reason unconfirmed) rather than reported as stable; a standing narrowing with it also withholds a favourable stability class; and our pages mark the record unconfirmed. Every other outcome leaves the record counting as it did before the check.","properties":{"checked":{"type":"string","format":"date","description":"The day we ran the check."},"outcome":{"type":"string","enum":["vendor_changed","ours","removal_stated_before","no_usable_capture","text_day_unknown","page_unreadable_today"],"description":"'vendor_changed' — archived copies show the vendor's page changing. 'ours' — the archived copy from when our listing text was written already states what the record calls new, so the difference was in our text, not on the vendor's page. 'removal_stated_before' — that archived copy already states the removal, so the removal predates our text. 'no_usable_capture' — no archived copy could settle the record either way. 'text_day_unknown' — we do not know when our listing text was written, so there was no capture to compare. 'page_unreadable_today' — we could not read the vendor's page when we ran the check, so no comparison ran."},"brackets":{"type":"array","description":"Where outcome is vendor_changed: each pair of archived copies between which the page changed, the last showing the old terms and the first showing the new. Where there is exactly one, date_meaning is effective_by: the change took effect after last_old and on or before date.","items":{"type":"object","properties":{"last_old":{"type":"string","format":"date","description":"The day of the last copy showing the old terms."},"first_new":{"type":"string","format":"date","nullable":true,"description":"The day of the first copy showing the new terms, or null where only our own read of the page shows them."},"last_old_capture":{"type":"string","format":"uri","nullable":true,"description":"That copy's address in the Internet Archive."},"first_new_capture":{"type":"string","format":"uri","nullable":true,"description":"That copy's address in the Internet Archive."}},"required":["last_old","first_new"]}},"capture_day":{"type":"string","format":"date","nullable":true,"description":"Where outcome is ours or removal_stated_before: the day of the archived copy from when our listing text was written."},"capture":{"type":"string","format":"uri","nullable":true,"description":"That copy's address in the Internet Archive."},"stated_then":{"type":"string","nullable":true,"description":"The line in that copy that settled the check, as the copy prints it: the terms the record called new (ours), or the removal (removal_stated_before)."},"recorded_as":{"type":"string","nullable":true,"enum":["free_tier_removed","open_source_killed","product_deprecated","limits_reduced","restriction","pricing_restructured","pricing_model_change","limits_increased","new_free_tier","new_tier","startup_program_expanded","pricing_postponed","rebranded","record_corrected"],"description":"Where outcome is ours: the change type our re-read recorded. The record's change_type is record_corrected."}},"required":["checked","outcome"]}},"required":["standing","date_meaning","ends_a_free_tier"]}],"description":"A change record as /api/changes serves it: every field of DealChange, plus a standing on every record rather than only on the ones we have withdrawn."},"ChangeDateWindow":{"type":"object","description":"The date filter this response was built under, stated whether or not one ran. A count of 0 means we hold no matching record only when applied is false; when applied is true it means we hold none dated on or after from, and the same query with a wider since may answer differently. The default 30-day window runs on the unfiltered feed alone — a request naming a type, vendor, vendors or category is answered from the whole change log.","properties":{"applied":{"type":"boolean","description":"Whether any date filter narrowed this response."},"from":{"type":"string","format":"date","nullable":true,"description":"The earliest date a record could carry and still be returned. Null when applied is false."},"source":{"type":"string","enum":["default","since_parameter","none"],"description":"'since_parameter' — your since value. 'default' — our window, because the request named no filter. 'none' — no date filter ran."},"field":{"type":"string","enum":["date"],"description":"The record field from compares against. date is the effective date only where date_meaning is \"effective\"; where it is \"discovered\", date is the day we recorded the change; where it is \"effective_by\", date is the latest day the change can have taken effect."},"note":{"type":"string","description":"The same statement in prose, for a caller reading the response rather than parsing it."}},"required":["applied","from","source","field","note"]},"ChangeResolution":{"type":"object","nullable":true,"description":"Present when the change this record describes is no longer in force. state is 'reversed' when the vendor ended it and 'retracted' when the record was our error. date is the day from which it stopped being in force, or the day we established that, whichever we can source. resolved_by names the change record that ended it, when one exists. A record carrying this field never rates the vendor.","properties":{"state":{"type":"string","enum":["reversed","retracted"]},"date":{"type":"string","format":"date"},"detail":{"type":"string"},"source_url":{"type":"string","format":"uri"},"resolved_by":{"type":"object","properties":{"vendor":{"type":"string"},"date":{"type":"string","format":"date"},"change_type":{"type":"string"}}}},"required":["state","date"]}}}}