{"openapi":"3.1.0","info":{"title":"Remedy API","version":"2026-11-05","description":"Recall refunds on a verified photo: search CPSC recalls, read a remedy as a proof rule, verify an owner's photo and pay the refund through PayPal. Sandbox only: no real money and no real personal data."},"servers":[{"url":"https://remedy-y0ym.onrender.com"}],"tags":[{"name":"Recalls and claims","description":"Recall search, proof rules, photo verification and claims. A key is optional for reads; with one you get higher limits and usage."},{"name":"Catalog scan","description":"Check a whole catalog against every recall since 2024 in one call."},{"name":"Hash pool","description":"The shared pool of photo hashes across firms: check one 64-bit hash, or register one your firm has paid on. Only the hash and an opaque ref cross."},{"name":"Embeds","description":"A server-rendered recall badge any product page can show in an iframe or with one script."},{"name":"Utilities","description":"The ledger check and service status."},{"name":"Keys","description":"Free sandbox keys."}],"paths":{"/api/v1/recalls":{"get":{"operationId":"recalls","summary":"Search recalls","description":"Search US consumer product recalls since 2024 in plain words, or list the newest that fit the filters. Each hit carries its proof rule in short.","tags":["Recalls and claims"],"security":[{"apiKey":[]},{}],"responses":{"200":{"description":"Success. The answer is in data; meta has request_id, version and latency_ms.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}},"parameters":[{"name":"query","in":"query","description":"Plain words, for example power bank or a crib that tips. Leave out to list the newest recalls.","schema":{"type":"string","example":"power bank"}},{"name":"photo_refund","in":"query","description":"true for recalls that offer a refund for a photo of the product made safe; false for the others.","schema":{"type":"string","enum":["true","false"],"example":"true"}},{"name":"year","in":"query","description":"Recall year, 2024 to 2026.","schema":{"type":"string"}},{"name":"category","in":"query","description":"Product type, for example Heaters or Baby and nursery.","schema":{"type":"string"}},{"name":"remedy","in":"query","description":"A CPSC remedy option.","schema":{"type":"string","enum":["Refund","Repair","Replace","Dispose","New Instructions","Label","Inspect"]}},{"name":"units","in":"query","description":"How many units were recalled.","schema":{"type":"string","enum":["under-1k","1k-10k","10k-50k","50k-plus"]}},{"name":"limit","in":"query","description":"Hits to return, 1 to 100. Default 20. A search with words returns at most 30.","schema":{"type":"string"}},{"name":"offset","in":"query","description":"Skip this many hits, for the next page.","schema":{"type":"string"}}],"x-remedy-scope":"read"}},"/api/v1/recalls/{number}":{"get":{"operationId":"recall","summary":"Get one recall","description":"One recall: its products, official images, hazard, the remedy exactly as published, and its proof rule with confidence per field.","tags":["Recalls and claims"],"security":[{"apiKey":[]},{}],"responses":{"200":{"description":"Success. The answer is in data; meta has request_id, version and latency_ms.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}},"parameters":[{"name":"number","in":"path","required":true,"description":"The CPSC recall number, for example 27056.","schema":{"type":"string"}}],"x-remedy-scope":"read"}},"/api/v1/proof-rule":{"post":{"operationId":"proofRule","summary":"Read a proof rule","description":"Turn a recall's remedy into a proof rule: what to cut or mark, what the photo must show, and the refund. Send a recall number for the stored rule, or any remedy text. With remedy_text, the quick reader answers at once with no AI. Add \"mode\": \"claude\" and an API key for Claude Opus 5.5 with a self-check, up to 20 a day per key.","tags":["Recalls and claims"],"security":[{"apiKey":[]},{}],"responses":{"200":{"description":"Success. The answer is in data; meta has request_id, version and latency_ms.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}},"x-remedy-scope":"verify","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"recall_number":{"description":"A CPSC recall number, for example \"27056\". Returns the stored rule for that recall.","type":"string","minLength":4,"maxLength":40},"remedy_text":{"description":"Any recall's remedy text, as the CPSC published it. Returns a rule read from it.","type":"string","minLength":20,"maxLength":8000},"consumer_contact":{"description":"The recall's consumer contact text, which helps find how the photo is sent. Optional.","type":"string","maxLength":2000},"mode":{"default":"quick","description":"For remedy_text: \"quick\" is the deterministic reader (no AI, instant). \"claude\" uses Claude Opus 5.5 with a self-check, needs an API key and counts toward a daily limit.","type":"string","enum":["quick","claude"]}}},"example":{"recall_number":"27056"}}}}}},"/api/v1/verify":{"post":{"operationId":"verify","summary":"Verify a photo","description":"Check an owner's photo against a recall's proof rule: right product, made safe, their own photo, within the recall. Returns each check and the decision. Nothing is paid.","tags":["Recalls and claims"],"security":[{"apiKey":[]},{}],"responses":{"200":{"description":"Success. The answer is in data; meta has request_id, version and latency_ms.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}},"x-remedy-scope":"verify","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["photo","recall_number"],"properties":{"photo":{"type":"string","format":"binary","description":"JPEG, PNG or WebP, up to 10 MB."},"recall_number":{"type":"string","description":"The CPSC recall number the photo is for."},"amount":{"type":"string","description":"The refund the firm would pay, for example 34.99. Optional; the decision then applies a $50 auto-approve cap."}}}}}}}},"/api/v1/label":{"post":{"operationId":"label","summary":"Check a label","description":"Is it recalled? Send a photo of the product's label: Claude reads the brand, product, model numbers, UPC and lot or date codes, each with the text read, and code looks them up in every recall since 2024. A model number or UPC in a recall's own list settles it. The photo is never stored. Claude reads; code decides. Fields whose value is not in the line it was read from, or read with confidence under 0.4, are set aside. Each model number read is one catalog-scan row with Claude off, so a label costs one model call. Live readings: 6 in 10 minutes and 20 a day per visitor, 25 a day per key, within the site's daily AI budget; the samples count against nothing.","tags":["Recalls and claims"],"security":[{"apiKey":[]},{}],"responses":{"200":{"description":"Success. The answer is in data; meta has request_id, version and latency_ms.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}},"x-remedy-scope":"read","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":[],"properties":{"photo":{"type":"string","format":"binary","description":"JPEG, PNG or WebP of the label, up to 10 MB. Leave out to send a sample instead."},"sample":{"type":"string","description":"Instead of a photo: a sample label, brightline, kidisle or lenovo. Replays its recorded reading with no model call. Also accepted as JSON."}}}}}}}},"/api/v1/claims":{"post":{"operationId":"claims","summary":"Open a claim","description":"Open a claim with the owner's photo. Remedy stores it, runs the six checks and records the decision. Paying is the next call. Sandbox only. Send Accept: text/event-stream to get each stage as it runs (claim, stage, result and error events). The same route as POST /api/claims, which the /claim page uses, with the same limits on live checks.","tags":["Recalls and claims"],"security":[{"apiKey":[]},{}],"responses":{"200":{"description":"Success. The answer is in data; meta has request_id, version and latency_ms.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}},"x-remedy-scope":"verify","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["photo","recall"],"properties":{"photo":{"type":"string","format":"binary","description":"JPEG or PNG of the product made safe, up to 15 MB."},"recall":{"type":"string","description":"The CPSC recall number, for example 27056."},"sale_id":{"type":"string","description":"The sample-shop purchase to refund, from the shop's receipt. Optional."},"capture_id":{"type":"string","description":"The PayPal capture to refund to, inside 180 days. Optional."},"paypal_email":{"type":"string","description":"Find the sample-shop purchase by the buyer's PayPal email instead. Optional."}}}}}}}},"/api/v1/claims/{id}":{"get":{"operationId":"claim","summary":"Claim status","description":"Where a claim stands: the six checks, the decision, and the payment with its PayPal status. The payee is masked. The same route as GET /api/claims/{id}.","tags":["Recalls and claims"],"security":[{"apiKey":[]},{}],"responses":{"200":{"description":"Success. The answer is in data; meta has request_id, version and latency_ms.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}},"parameters":[{"name":"id","in":"path","required":true,"description":"The claim id returned when it was opened.","schema":{"type":"string"}},{"name":"refresh","in":"query","description":"1 asks PayPal for the payment's latest status first.","schema":{"type":"string","enum":["1"]}}],"x-remedy-scope":"read"}},"/api/v1/claims/{id}/pay":{"post":{"operationId":"pay","summary":"Pay a claim","description":"Say where the refund goes, then pay it in the PayPal sandbox: a payout to PayPal or Venmo, a refund to the original PayPal purchase inside 180 days, or a safe replacement. A linked purchase is refunded to its capture and needs no payee. The firm's auto rule pays a verified claim at or under its cap at once; anything else waits for a person at the firm. A second call never pays twice. The same route as POST /api/claims/{id}/pay.","tags":["Recalls and claims"],"security":[{"apiKey":[]},{}],"responses":{"200":{"description":"Success. The answer is in data; meta has request_id, version and latency_ms.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}},"parameters":[{"name":"id","in":"path","required":true,"description":"The claim id.","schema":{"type":"string"}}],"x-remedy-scope":"payments:sandbox","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":[],"properties":{"choice":{"type":"string","description":"money (with payee_type and payee) or replacement (with replacement_id). Default money."},"payee_type":{"type":"string","description":"Where a money refund goes. Venmo falls back to a PayPal phone payout in the sandbox."},"payee":{"type":"string","description":"The owner's PayPal email or phone number. Stored encrypted; shown masked."},"replacement_id":{"type":"string","description":"A product id from the replacements call."}}},"example":{"choice":"money","payee_type":"email","payee":"jordan@example.com"}}}}}},"/api/v1/claims/{id}/replacements":{"get":{"operationId":"replacements","summary":"Safe replacements","description":"Products a verified owner can choose instead of money, found by Channel3, each checked against active recalls first. The same route as GET /api/claims/{id}/replacements.","tags":["Recalls and claims"],"security":[{"apiKey":[]},{}],"responses":{"200":{"description":"Success. The answer is in data; meta has request_id, version and latency_ms.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}},"parameters":[{"name":"id","in":"path","required":true,"description":"The claim id.","schema":{"type":"string"}}],"x-remedy-scope":"read"}},"/api/v1/scan":{"post":{"operationId":"scan","summary":"Scan a catalog","description":"Check a whole catalog at once. Each row comes back photo_refund (a recall pays for a photo of it made safe), recalled (a recall with another remedy), clear or unknown, with the recall and the claim link. Up to 50 rows without a key, 500 with one. The body can also be a CSV (Content-Type text/csv, or a multipart file field) with name, model, sku and brand columns. A stated model or UPC that appears in a recall's own list settles a row without AI; Claude Opus 5.5 reads only the ambiguous rows, in batches, within the site's daily AI budget.","tags":["Catalog scan"],"security":[{"apiKey":[]},{}],"responses":{"200":{"description":"Success. The answer is in data; meta has request_id, version and latency_ms.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}},"parameters":[{"name":"format","in":"query","description":"csv returns the results as a CSV file instead of JSON.","schema":{"type":"string","enum":["csv"]}},{"name":"ai","in":"query","description":"With a CSV body: false skips Claude for the ambiguous rows.","schema":{"type":"string","enum":["false"]}}],"x-remedy-scope":"read","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"items":{"minItems":1,"maxItems":500,"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200,"description":"The product as listed, for example \"HAIARA 10000mAh power bank\"."},"model":{"description":"The model number, when known. A model in the recall's own list settles the row.","anyOf":[{"type":"string","maxLength":80},{"type":"null"}]},"sku":{"description":"Your SKU or the UPC. A UPC in the recall's list settles the row.","anyOf":[{"type":"string","maxLength":80},{"type":"null"}]},"brand":{"description":"The brand or maker, when it is not in the name.","anyOf":[{"type":"string","maxLength":80},{"type":"null"}]}},"required":["name"]},"description":"Up to 500 rows (50 without an API key)."},"ai":{"default":true,"description":"False skips Claude for the ambiguous rows, which then come back unknown. Default true.","type":"boolean"}},"required":["items"]},"example":{"items":[{"name":"HAIARA 10000mAh power bank","brand":"HAIARA"},{"name":"Bamboo cutting board, 18 by 12 inch","brand":"Greenfield Kitchen"}]}}}}}},"/api/v1/scan/sample":{"get":{"operationId":"scanSample","summary":"Sample catalog","description":"A small catalog built from real recalls in Remedy's data plus three everyday items no recall covers, ready to send to the scan.","tags":["Catalog scan"],"security":[{"apiKey":[]},{}],"responses":{"200":{"description":"Success. The answer is in data; meta has request_id, version and latency_ms.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}},"x-remedy-scope":"read"}},"/api/v1/hash-pool/check":{"post":{"operationId":"hashPoolCheck","summary":"Check the hash pool","description":"Ask whether a photo is already in the shared pool: every photo claimed on any recall at any firm, every hash a connected firm registered, and the published pictures of every recall. Send the 64-bit hash only; the answer is a hit or no hit, the distance and an opaque ref. Send the upright 64-bit dHash of the photo (the pipeline is documented in src/lib/verify/dhash.ts: greyscale, fit inside 256 by 256, a 9 by 8 grid, one bit per row neighbour). The pool holds upright hashes and compares your one hash with them, so hash the photo as it stands. No photo crosses, and nothing about the holder comes back but the ref.","tags":["Hash pool"],"security":[{"apiKey":[]},{}],"responses":{"200":{"description":"Success. The answer is in data; meta has request_id, version and latency_ms.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}},"x-remedy-scope":"read","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"dhash":{"type":"string","pattern":"^[0-9a-f]{16}$","description":"The 64-bit perceptual hash of the photo, upright, as 16 lowercase hex characters (the dHash in src/lib/verify/dhash.ts)."},"phash":{"description":"Optional: the second 64-bit hash of the photo's center (src/lib/verify/phash.ts), which holds under a small crop or turn. Matched against claim and partner rows that carry one, at 6 bits.","type":"string","pattern":"^[0-9a-fA-F]{16}$"},"recall_number":{"description":"The recall the photo is claimed on. With it, that recall's own earlier claim photos and published pictures are matched at 10 bits; everything else at 4.","type":"string","minLength":4,"maxLength":40}},"required":["dhash"]},"example":{"dhash":"337bfb9f4e7f3e0f","recall_number":"FW-2026-01"}}}}}},"/api/v1/hash-pool/register":{"post":{"operationId":"hashPoolRegister","summary":"Register a hash","description":"Add the hash of a photo your firm has paid a claim on, so the same photo sent to any other firm is held for a person before any model call. Only the hash, your recall number and your own opaque ref are stored. Needs a firm key: a key tied to a firm connected to Remedy, with the hash-pool:register scope, issued to the firm when it connects. A self-serve sandbox key is answered 403 forbidden; checking needs no key. The firm is recorded in Remedy's audit ledger, never in the pool row, so a hit at another firm can never name yours. A hash within 4 bits of one registered, sent on any other recall at any firm, is held for a person at that firm.","tags":["Hash pool"],"security":[{"apiKey":[]},{}],"responses":{"200":{"description":"Success. The answer is in data; meta has request_id, version and latency_ms.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}},"x-remedy-scope":"hash-pool:register","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"dhash":{"type":"string","pattern":"^[0-9a-f]{16}$","description":"The 64-bit perceptual hash of the photo, upright, as 16 lowercase hex characters (the dHash in src/lib/verify/dhash.ts)."},"phash":{"description":"Optional: the second 64-bit hash of the photo's center (src/lib/verify/phash.ts), which holds under a small crop or turn. Matched against claim and partner rows that carry one, at 6 bits.","type":"string","pattern":"^[0-9a-fA-F]{16}$"},"recall_number":{"type":"string","minLength":4,"maxLength":40,"description":"The recall the photo was claimed on, as your firm names it."},"ref":{"description":"Your own opaque id for the row, up to 64 characters; it is the only thing another firm sees on a hit. Left out, Remedy makes a UUID.","type":"string","minLength":1,"maxLength":64,"pattern":"^[\\w.:-]+$"}},"required":["dhash","recall_number"]},"example":{"dhash":"337bfb9f4e7f3e0f","recall_number":"BH-2026-01","ref":"partner-demo-1"}}}}}},"/api/embed/badge":{"get":{"operationId":"badge","summary":"Recall badge","description":"A small server-rendered page for an iframe: the recall's one-line remedy and refund, with a Get your refund link. Light and dark follow the visitor's setting. Embed it with <iframe src=\"/api/embed/badge?recall=27056\" title=\"Recall badge\" loading=\"lazy\" style=\"border:0;width:100%;max-width:28rem;height:7.5rem\"></iframe>, or load /api/embed/badge.js with data-recall=\"27056\" on the script tag to insert the same badge where the script sits.","tags":["Embeds"],"security":[],"responses":{"200":{"description":"Success. The answer is in data; meta has request_id, version and latency_ms.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"default":{"$ref":"#/components/responses/Error"}},"parameters":[{"name":"recall","in":"query","description":"A CPSC recall number, for example 27056.","schema":{"type":"string","example":"27056"}},{"name":"q","in":"query","description":"Or the product in plain words; the best-matching recall is shown.","schema":{"type":"string"}}]}},"/api/ledger/verify":{"get":{"operationId":"ledger","summary":"Verify the ledger","description":"Recompute the audit ledger's SHA-256 hash chain from the first entry and report whether every entry still matches.","tags":["Utilities"],"security":[],"responses":{"200":{"description":"Success. The answer is in data; meta has request_id, version and latency_ms.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/api/health":{"get":{"operationId":"health","summary":"Service status","description":"Whether PayPal, Claude, the database and each tool Remedy is built on are live, on a fallback, or not connected right now.","tags":["Utilities"],"security":[],"responses":{"200":{"description":"Success. The answer is in data; meta has request_id, version and latency_ms.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/api/keys":{"post":{"operationId":"createKey","summary":"Create a sandbox API key","description":"Returns the full key once. Only its SHA-256 hash is stored. Limits: 5 new keys per client and 3 per email each day.","tags":["Keys"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":80,"description":"Your name or your team's."},"email":{"type":"string","maxLength":200,"format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$","description":"Where to reach you about the key."},"purpose":{"description":"What you are building. Optional.","type":"string","maxLength":200},"accept":{"type":"boolean","const":true,"description":"You accept that sandbox keys are for testing, with no real money and no real personal data."}},"required":["name","email","accept"]}}}},"responses":{"200":{"description":"The new key, shown once.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"default":{"$ref":"#/components/responses/Error"}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"An rm_test_ key from the Developers page."}},"schemas":{"Envelope":{"type":"object","properties":{"data":{"description":"The answer. Null on error."},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"version":{"type":"string"},"latency_ms":{"type":"integer"}}},"error":{"$ref":"#/components/schemas/Error"}}},"Error":{"type":["object","null"],"properties":{"code":{"type":"string","enum":["invalid_request","unauthorized","forbidden","not_found","rate_limited","upstream_unavailable","internal"]},"message":{"type":"string","description":"A plain sentence you can show to people."},"docs":{"type":"string"},"details":{}}}},"responses":{"Error":{"description":"An error, with a plain message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"RateLimited":{"description":"Too many requests. Retry after the number of seconds in Retry-After.","headers":{"Retry-After":{"schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}}}}}