# Until > No-signup, prepaid "wake me when it happens" watch API (price drops, item back in stock, page changes) for AI agents. POST a URL and a deterministic condition, pay once ($0.50/watch at list price from prepaid credits; larger packs cost less per watch), and get a watch_id. Poll GET /v1/watch/{id} (or receive a signed webhook) to get a HMAC-signed evidence receipt (matched value, timestamp, content hash) once the condition is true. ## How it works 1. (optional, free) POST https://untilapi.com/v1/check {"url","condition"} - dry-run: is the page watchable, and is the condition true now? 2. Buy credits once: POST https://untilapi.com/v1/credits/checkout {"bundle":5|20|50|100|250} -> open checkout_url (card); save the returned credit_key. Packs: $5=10 watches ($0.50 each); $20=50 watches ($0.40 each, save 20%); $50=150 watches ($0.33 each, save 33%); $100=300 watches ($0.33 each, save 33%); $250=800 watches ($0.31 each, save 37%). Credits never expire. 3. POST https://untilapi.com/v1/watch (Authorization: Bearer ) {"url":"https://shop.example/item","condition":{"type":"price","op":"<","value":50},"expires_in":"7d","webhook":"https://you.example/hook"} -> 201 {watch_id, status_url, webhook_secret}. Without credit: 402 with payment instructions. 4. GET https://untilapi.com/v1/watch/{watch_id} -> {state: active|matched|expired|failed, receipt?} active = still checking, no receipt yet; matched = condition true, receipt included; expired = time ran out without a match; failed = page could not be fetched usably (see failure_code). ## Conditions (deterministic; no LLM; static HTML/JSON only) - {"type":"price","op":"<","value":50} (JSON-LD offers, price meta tags, itemprop=price) - {"type":"in_stock"} (needs structured availability; see "in_stock" below. Optional "variant_id" for a Shopify variant) - {"type":"text_appears","text":"Tickets on sale"} / {"type":"text_absent","text":"Sold out"} - {"type":"http_status","op":"==","value":200} - {"type":"json_path","path":"$.data.price","op":"<","value":10} (ops: < <= > >= == != exists not_exists contains) - {"type":"header","name":"etag","op":"exists"} - {"type":"all"|"any","conditions":[...up to 5]} ## in_stock: what it parses in_stock is deterministic and needs structured data; it never guesses from visible text ("Sold out" buttons etc.). - Shopify product URLs (/products/, also /products/.json, optional /collections/x/ and locale prefixes): Until reads the store's /products/.js (robots.txt permitting), which has a boolean `available` per variant. No variant given => in stock if ANY variant is available. Choose one variant with {"type":"in_stock","variant_id":123} or ?variant=123 in the URL (variant_id wins). Observed value: InStock / OutOfStock. - Everything else: schema.org Offer `availability` in JSON-LD, or a product:availability / og:availability meta tag (InStock, OutOfStock, SoldOut, PreOrder, LimitedAvailability...). - If none is found the condition is "not evaluable" (dry-run: accepted:false; real watch: 422 condition_not_evaluable, not charged). Use text_appears / json_path on a page that exposes the state instead. Shopify /products/.json alone has no availability field. ## Dry-run vs real watch responses POST /v1/check returns HTTP 200 for any completed fetch, even when a real watch would be refused: read `accepted` (false = page unusable or condition not evaluable), `page`, `evaluation` and `warning`. `warning` is null, or {code, would_http_status, message}: already_true / 409 when matches_now is true, or 422 (bot_blocked, js_required, condition_not_evaluable, ...). Only POST /v1/watch actually returns 409/422. Headers X-RateLimit-Limit / -Remaining / -Reset (epoch seconds) on every /v1/check response; 429 also sends Retry-After. Counters are per server instance (best-effort). ## Not charged (refused with 4xx) Bot-blocked / captcha pages, JavaScript-only pages, robots.txt-disallowed paths, unreachable URLs, conditions that cannot be evaluated on the page, and conditions that are already true (unless allow_already_true). A watch is only created (and charged) after a successful first fetch, so refusals happen before any charge. Once created, a watch costs one watch of credit whether or not the condition becomes true before it expires. ## Receipts Receipt = {payload:{watch_id,url,final_url,condition,matched_value,matched_at,http_status,content_sha256}, signature, alg:"HMAC-SHA256"}. Verify with POST https://untilapi.com/v1/receipt/verify. Webhooks carry header Until-Signature: t=,v1=.")>. ## Limits Max watch duration 7 days; check interval 300-86400 s (default 900 = 15 min, minimum 300 = 5 min). Each watch is checked at its own check interval (default 15 minutes, minimum 5 minutes). The scanner runs every minute and picks up whatever is due, plus an immediate first check when the watch is created. Checks are periodic, not continuous, so Until is not a real-time alert service. 2 MB body cap; ports 80/443/8080/8443; private/internal addresses are blocked. ## Interfaces - OpenAPI: https://untilapi.com/openapi.json - MCP (Streamable HTTP, stateless, JSON responses; protocol 2026-07-28, also 2025-11-25/2025-06-18/2025-03-26): https://untilapi.com/mcp tools: test_condition (free), create_watch (needs credit key: Authorization: Bearer header or api_key argument), get_watch. No OAuth.