{"openapi":"3.1.0","info":{"title":"CreditSync API","version":"1.0","description":"Agent-first REST API for CreditSync — track credit card benefits, welcome offers, loyalty balances, annual fee ROI, and application eligibility (Chase 5/24). Designed for AI assistants and automation: snake_case JSON, explicit enums, machine-readable urgency and provenance fields, and dry_run flags on bulk/create operations. Also exposed as MCP tools via @creditsync-pro/mcp. Base URL: https://www.creditsync.pro"},"servers":[{"url":"https://www.creditsync.pro"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Personal access token (PAT), prefixed with 'cs_'. Create one at https://www.creditsync.pro/settings/api — requires a Pro subscription. Invalid, inactive, or expired tokens return 401; a valid token whose household subscription no longer includes API access returns 403 ('rotate the token' vs 'renew the subscription')."}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]},"WelcomeOffer":{"type":"object","description":"Serialized welcome offer (lib/api-v1/welcome-offer-utils.ts#serializeOffer).","properties":{"offer_id":{"type":"string"},"card_id":{"type":"string"},"card_display_name":{"type":"string"},"owner":{"type":"string"},"bonus_amount":{"type":"number"},"bonus_currency":{"type":"string","description":"Loyalty currency key (e.g. amex_mr, chase_ur) or 'cashback'."},"bonus_value":{"type":["number","null"],"description":"Bonus normalized to dollars at the resolved CPP."},"bonus_value_source":{"type":"string","enum":["user","fallback","cashback"],"description":"Provenance of the CPP used for bonus_value: 'user' = household-customized valuation, 'fallback' = market-consensus default, 'cashback' = hard dollars (100 cents per unit). Lets agents distinguish a hard number from a market estimate."},"spend_requirement":{"type":"number"},"current_spend":{"type":"number"},"spend_remaining":{"type":"number"},"spend_deadline":{"type":"string","format":"date"},"days_remaining":{"type":"integer"},"daily_spend_needed":{"type":["number","null"],"description":"spend_remaining / days_remaining; null when the deadline has passed."},"spend_requirement_met":{"type":"boolean"},"is_at_risk":{"type":"boolean"},"urgency":{"type":"string","enum":["met","at_risk","heads_up","on_track"]},"status":{"type":"string","description":"in_progress | earned | missed | cancelled"},"notes":{"type":["string","null"]},"origin":{"type":"string","description":"'signup' (default) or 'retention' for offers auto-paired from an accepted retention offer."},"source_event_id":{"type":["string","null"],"description":"CardEvent id of the paired retention offer when origin='retention'."}}},"BenefitListItem":{"type":"object","properties":{"benefit_id":{"type":"string"},"period_id":{"type":"string"},"credit_name":{"type":"string"},"card_display_name":{"type":"string"},"owner":{"type":"string"},"period_type":{"type":"string","description":"Schedule type: monthly, quarterly, semiannual, annual_calendar, annual_cardmember, one_time."},"period_label":{"type":"string","description":"Human label, e.g. 'Q3 2026' or 'Jul 2026'."},"credit_amount":{"type":"number"},"amount_used":{"type":"number"},"amount_remaining":{"type":"number"},"unit":{"type":"string","description":"e.g. 'dollars'."},"period_start":{"type":"string","format":"date"},"period_end":{"type":"string","format":"date"},"days_remaining":{"type":"integer"},"status":{"type":"string","enum":["unused","partial","used"]},"category":{"type":"string"},"benefit_group":{"type":["string","null"],"description":"Dedup group key (e.g. 'clear', 'global_entry') when the benefit reimburses only once per household/owner."},"benefit_group_scope":{"type":["string","null"],"description":"'household' or 'owner'."},"benefit_group_suppressed":{"type":"boolean","description":"True when a sibling card's period is the group winner — this entry is covered elsewhere. The list is NOT filtered; aggregating callers should skip suppressed rows or use /api/v1/dashboard (already deduped)."},"benefit_group_suppressed_by":{"type":["object","null"],"properties":{"card_id":{"type":"string"},"card_display_name":{"type":"string"},"reason":{"type":"string","description":"'covered_by_household' or 'covered_by_owner'."}},"additionalProperties":true}}},"BenefitPeriod":{"type":"object","properties":{"period_id":{"type":"string"},"period_start":{"type":"string","format":"date"},"period_end":{"type":"string","format":"date"},"amount":{"type":"number"},"amount_used":{"type":"number"},"amount_remaining":{"type":"number"},"unit":{"type":"string"},"status":{"type":"string","enum":["unused","partial","used"]},"used_at":{"type":["string","null"],"format":"date-time"},"notes":{"type":["string","null"]}}},"ActionItem":{"type":"object","description":"Prioritized action item (lib/queries/action-items.ts#ActionItem).","properties":{"priority":{"type":"string","enum":["critical","high","medium","low"]},"type":{"type":"string","enum":["benefit_expiring","unused_prior_period","fee_renewal","negative_roi","welcome_offer_at_risk","at_risk_this_period"]},"title":{"type":"string"},"detail":{"type":"string"},"value_at_risk":{"type":["number","null"]},"days_remaining":{"type":["integer","null"]},"card_display_name":{"type":"string"},"benefit_id":{"type":["string","null"]},"period_id":{"type":["string","null"]},"card_id":{"type":["string","null"]},"fee_amount":{"type":"number","description":"Only on fee_renewal items."},"recommended_action":{"type":"string","enum":["cancel","retention_call","hold"],"description":"Populated for fee_renewal and negative_roi items."},"retention_leverage":{"type":"string","enum":["weak","moderate","strong"],"description":"fee_renewal only: weak = captured >=85%, moderate = 50-85%, strong = <50%."},"historical_offers":{"type":"array","description":"Prior retention offers on this card, most-recent first (fee_renewal only, up to 3).","items":{"type":"object","properties":{"event_id":{"type":"string"},"date":{"type":"string","format":"date"},"description":{"type":"string"},"offer_type":{"type":["string","null"]},"offer_value_dollars":{"type":["number","null"]},"offer_points":{"type":["number","null"]},"offer_currency":{"type":["string","null"]},"outcome":{"type":["string","null"]}}}},"historical_rate":{"type":"number","description":"Dollar-weighted historical capture rate in [0,1] (at_risk_this_period only)."},"historical_rate_n":{"type":"integer"},"historical_rate_confidence":{"type":"string","enum":["high","low"]}},"additionalProperties":true},"CalendarEvent":{"type":"object","properties":{"event_type":{"type":"string","enum":["expiry","credit_reset","annual_fee_due","benefit_available","welcome_offer_deadline","loyalty_program_expiry","reward_expiry"]},"title":{"type":"string","description":"Retention-origin welcome_offer_deadline titles carry a '[Retention] ' prefix."},"date":{"type":"string","format":"date"},"end_date":{"type":["string","null"],"format":"date"},"all_day":{"type":"boolean"},"description":{"type":"string"},"recurrence":{"type":["string","null"],"description":"RRULE string for annual_fee_due events."},"alert_days_before":{"type":"integer"},"value_at_risk":{"type":["number","null"]},"benefit_id":{"type":["string","null"]},"period_id":{"type":["string","null"]},"card_id":{"type":["string","null"]},"card_display_name":{"type":["string","null"]},"status":{"type":["string","null"]},"unit":{"type":["string","null"]},"origin":{"type":["string","null"],"description":"Only on welcome_offer_deadline events: 'signup' or 'retention'."},"source_event_id":{"type":["string","null"],"description":"Only on welcome_offer_deadline events with origin='retention'."}}},"CardListItem":{"type":"object","properties":{"card_id":{"type":"string"},"card_display_name":{"type":"string"},"issuer":{"type":"string","description":"Marketing brand (Hilton, Amex, Chase)."},"issuing_bank":{"type":["string","null"],"description":"Actual issuing bank — drives 5/24 and 2/90 application rules."},"card_name":{"type":"string"},"nickname":{"type":["string","null"]},"last_four":{"type":["string","null"]},"owner":{"type":"string"},"annual_fee":{"type":"number"},"renewal_month":{"type":["string","null"],"description":"Month name, e.g. 'March'."},"benefit_count":{"type":"integer"},"added_at":{"type":"string","format":"date"}}},"RetentionEntry":{"type":"object","description":"Retention offer record (created or updated). Same field set for retention_entry and updated_retention_event.","properties":{"event_id":{"type":"string"},"date":{"type":"string","format":"date"},"offer":{"type":"string"},"accepted":{"type":"boolean","description":"Legacy field: outcome === 'accepted'."},"notes":{"type":["string","null"]},"offer_type":{"type":["string","null"],"description":"statement_credit | points_bonus | fee_waiver | mq_extension | spend_bonus | other | no_offer"},"offer_value_dollars":{"type":["number","null"]},"offer_points":{"type":["number","null"]},"offer_currency":{"type":["string","null"]},"outcome":{"type":["string","null"],"description":"accepted | declined | countered | escalated | no_offer; null = pending."},"paired_welcome_offer_id":{"type":"string","description":"Present when an accepted offer with spend_requirement + spend_deadline auto-created a paired retention-origin WelcomeOffer."},"pairing_skipped_reason":{"type":"string","description":"Additive field: present when pairing conditions were met but skipped (e.g. the card already has an in-progress welcome offer)."}},"additionalProperties":true},"Reward":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","description":"e.g. gift_card, voucher, travel_credit, other."},"value":{"type":["number","null"]},"currency":{"type":"string"},"expiration_date":{"type":["string","null"],"format":"date"},"notes":{"type":["string","null"]},"status":{"type":"string","description":"active | used | expired."},"source":{"type":["string","null"]},"owner":{"type":["string","null"]},"used_at":{"type":["string","null"],"format":"date"},"created_at":{"type":"string","format":"date"}},"additionalProperties":true},"LoyaltyProgram":{"type":"object","properties":{"program_key":{"type":"string"},"display_name":{"type":"string"},"type":{"type":"string","description":"Program type, e.g. 'bank', 'airline', 'hotel'."},"currency":{"type":"string"},"balance":{"type":"number"},"cpp_valuation":{"type":"number","description":"Cents per point."},"estimated_value":{"type":"number","description":"balance * cpp / 100, in dollars."},"last_updated":{"type":"string","format":"date"},"days_since_update":{"type":"integer"},"is_stale":{"type":"boolean","description":"days_since_update > stale_days (default threshold 30 when stale_days is omitted)."},"owner":{"type":["string","null"]},"linked_cards":{"type":"array","items":{"type":"string"}},"transfer_partner_count":{"type":"integer"},"expiration_date":{"type":["string","null"],"format":"date"},"expiration_policy":{"type":["string","null"]},"notes":{"type":["string","null"]}}},"CatalogCard":{"type":"object","properties":{"card_template_id":{"type":"string"},"issuer":{"type":"string"},"name":{"type":"string"},"annual_fee":{"type":"number"},"is_premium":{"type":"boolean"},"benefit_count":{"type":"integer"}}},"BulkCardResult":{"type":"object","properties":{"index":{"type":"integer"},"input_name":{"type":"string"},"status":{"type":"string","enum":["created","already_exists","ambiguous","failed"]},"card_id":{"type":"string"},"card_display_name":{"type":"string"},"matched_template":{"type":"object","properties":{"card_template_id":{"type":"string"},"issuer":{"type":"string"},"issuing_bank":{"type":["string","null"]},"name":{"type":"string"},"annual_fee":{"type":"number"},"benefit_count":{"type":"integer"}}},"fee_month_inferred":{"type":"boolean"},"welcome_offer_id":{"type":"string"},"welcome_offer_warning":{"type":"string"},"candidates":{"type":"array","description":"Present when status='ambiguous' — fuzzy match found multiple templates.","items":{"type":"object","properties":{"card_template_id":{"type":"string"},"issuer":{"type":"string"},"name":{"type":"string"},"annual_fee":{"type":"number"},"benefit_count":{"type":"integer"},"match_score":{"type":"number"}}}},"error":{"type":"string"}},"additionalProperties":true}},"responses":{"Unauthorized":{"description":"Invalid, inactive, or expired API token (or missing 'Bearer cs_...' header).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"Token is valid but the household's Pro subscription no longer grants API access (or a tier limit was hit).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Resource not found or not owned by this household.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"BadRequest":{"description":"Validation error — the message names the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/api/v1/openapi.json":{"get":{"summary":"This OpenAPI specification","description":"Returns this OpenAPI 3.1 document as JSON. No authentication required.","security":[],"responses":{"200":{"description":"The OpenAPI 3.1 specification.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/api/v1/benefits":{"get":{"summary":"List tracked benefits with current-period status","description":"One row per tracked benefit, showing its current (or next upcoming) period. Suppressed benefit-group siblings (CLEAR, Global Entry) are annotated via benefit_group_suppressed but NOT filtered out. Response shape changes when expiring_within_days is passed: instead of a bare array, returns { total_at_risk, credit_count, credits }.","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["unused","partial","used"]},"description":"Filter by current-period usage status."},{"name":"card","in":"query","schema":{"type":"string"},"description":"Case-insensitive partial match on card nickname. Ignored when card_id is present."},{"name":"card_id","in":"query","schema":{"type":"string"},"description":"Exact card id — takes precedence over 'card'."},{"name":"expiring_within_days","in":"query","schema":{"type":"integer","minimum":0},"description":"Only benefits whose current period ends within N days. Also switches the response to the wrapper shape { total_at_risk, credit_count, credits }. Non-integer or negative values return 400."}],"responses":{"200":{"description":"Array of benefits (default), or { total_at_risk, credit_count, credits } when expiring_within_days is passed.","content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/BenefitListItem"}},{"type":"object","properties":{"total_at_risk":{"type":"number"},"credit_count":{"type":"integer"},"credits":{"type":"array","items":{"$ref":"#/components/schemas/BenefitListItem"}}}}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"summary":"Create an agent-added benefit with auto-generated periods","description":"Creates a custom benefit on a card and generates its periods (18-month lookahead). Returns 409 if a benefit with the same name (custom creditName OR catalog-backed template name, case-insensitive) already exists on this card — the 409 body includes existing_benefit_id so agents can switch to PATCH. Pass force: true to intentionally add a duplicate. Pass dry_run: true to validate without persisting (returns would_create instead of benefit_id — used for smoke tests).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["card_id","credit_name","amount","schedule_type"],"properties":{"card_id":{"type":"string"},"credit_name":{"type":"string"},"amount":{"type":"number","minimum":0},"unit":{"type":"string","default":"dollars"},"schedule_type":{"type":"string","enum":["monthly","quarterly","semiannual","annual_calendar","annual_cardmember","one_time"]},"category":{"type":"string","default":"other"},"template_benefit_id":{"type":"string"},"period_start":{"type":"string","format":"date"},"notes":{"type":"string"},"force":{"type":"boolean","description":"Bypass the 409 duplicate-name guard."},"dry_run":{"type":"boolean","description":"Validate without persisting."}}}}}},"responses":{"200":{"description":"Dry-run validation result (only when dry_run: true).","content":{"application/json":{"schema":{"type":"object","properties":{"dry_run":{"type":"boolean","const":true},"credit_name":{"type":"string"},"card_display_name":{"type":"string"},"amount":{"type":"number"},"unit":{"type":"string"},"schedule_type":{"type":"string"},"category":{"type":"string"},"would_create":{"type":"boolean"},"duplicate_warning":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"201":{"description":"Benefit created.","content":{"application/json":{"schema":{"type":"object","properties":{"benefit_id":{"type":"string"},"credit_name":{"type":"string"},"card_display_name":{"type":"string"},"amount":{"type":"number"},"unit":{"type":"string"},"schedule_type":{"type":"string"},"category":{"type":"string"},"periods_generated":{"type":"integer"},"is_agent_created":{"type":"boolean"},"duplicate_warning":{"type":["string","null"],"description":"Set when force: true bypassed an existing duplicate."},"success":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"A benefit with this name already exists on the card. Retry with force: true to add intentionally, or PATCH the existing benefit.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"existing_benefit_id":{"type":"string"},"existing_credit_name":{"type":"string"},"is_catalog_backed":{"type":"boolean"}}}}}}}}},"/api/v1/benefits/{id}":{"get":{"summary":"Single benefit detail with all periods","parameters":[{"name":"id","in":"path","required":true,"description":"UserBenefit id.","schema":{"type":"string"}}],"description":"Returns the benefit plus every generated period (past and future), ordered by period_start.","responses":{"200":{"description":"Benefit detail.","content":{"application/json":{"schema":{"type":"object","properties":{"benefit_id":{"type":"string"},"credit_name":{"type":"string"},"card_display_name":{"type":"string"},"owner":{"type":"string"},"category":{"type":"string"},"schedule_type":{"type":"string"},"description":{"type":"string"},"periods":{"type":"array","items":{"$ref":"#/components/schemas/BenefitPeriod"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"summary":"Update benefit details (name, amount, category, status)","description":"Renames only apply to custom (agent-created) benefits — attempting to rename a catalog-backed benefit returns 422 (the name comes from the immutable catalog template). Amount changes propagate to current + future unused periods. status: 'discontinued' also flips isTracked=false so the benefit disappears from every aggregate surface. All changes are audit-logged as CardEvents and echoed in the changes[] array.","parameters":[{"name":"id","in":"path","required":true,"description":"UserBenefit id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"credit_name":{"type":"string","description":"Custom benefits only — 422 on catalog-backed."},"amount":{"type":"number","minimum":0},"category":{"type":"string"},"notes":{"type":"string"},"status":{"type":"string","enum":["active","discontinued"]}}}}}},"responses":{"200":{"description":"Update result with change log.","content":{"application/json":{"schema":{"type":"object","properties":{"benefit_id":{"type":"string"},"changes":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"old_value":{"type":["string","null"]},"new_value":{"type":["string","null"]}}}},"success":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"credit_name cannot be changed on a catalog-backed benefit (name comes from the immutable catalog template).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/benefits/{id}/use":{"patch":{"summary":"Mark a benefit period used, partial, or unused","description":"Operates on a BenefitPeriod id (from period_id fields elsewhere), not a benefit id. usageStatus 'used' defaults usedAmount to the full period amount; 'partial' requires 0 < usedAmount < amount; 'unused' clears usedAt.","parameters":[{"name":"id","in":"path","required":true,"description":"BenefitPeriod id (period_id).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["usageStatus"],"properties":{"usageStatus":{"type":"string","enum":["unused","partial","used"]},"usedAmount":{"type":"number","description":"Required for 'partial' (must be < period amount); optional for 'used' (defaults to full amount)."},"notes":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated period.","content":{"application/json":{"schema":{"type":"object","properties":{"period_id":{"type":"string"},"status":{"type":"string","enum":["unused","partial","used"]},"amount":{"type":"number"},"amount_used":{"type":"number"},"amount_remaining":{"type":"number"},"used_at":{"type":["string","null"],"format":"date-time"},"success":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/dashboard":{"get":{"summary":"Dashboard metrics: expiring, captured, available, ROI","description":"Household-level aggregates. Benefit-group dedup (CLEAR/Global Entry) is already applied, so totals match the web dashboard. Dates are Pacific-anchored.","responses":{"200":{"description":"Dashboard metrics.","content":{"application/json":{"schema":{"type":"object","properties":{"expiring_in_30_days":{"type":"object","properties":{"amount":{"type":"number"},"count":{"type":"integer"}}},"captured_this_month":{"type":"number"},"total_available":{"type":"number"},"net_value_ytd":{"type":"object","properties":{"total_fees":{"type":"number"},"total_captured":{"type":"number"},"net_value":{"type":"number"}}},"card_count":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/cards":{"get":{"summary":"List cards with fees, renewal months, benefit counts","description":"All active (non-archived) cards in the household, oldest first. renewal_month is a month name derived from fee_month.","responses":{"200":{"description":"Active (non-archived) cards, oldest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CardListItem"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"summary":"Add a card to the portfolio with full metadata in one call","description":"Resolve the catalog template by fuzzy 'name' or exact 'card_template_id'. When a fuzzy name matches multiple templates, returns HTTP 300 with a candidates list — retry with card_template_id. fee_month is auto-inferred from original_open_date when omitted (fee_month_inferred: true in the response). Business cards default counts_toward_524 to false. Free tier is limited to 2 cards (403 when exceeded).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["owner"],"properties":{"name":{"type":"string","description":"Fuzzy template search — either this or card_template_id is required."},"card_template_id":{"type":"string","description":"Exact template id."},"owner":{"type":"string","description":"Owner profile name (case-insensitive)."},"nickname":{"type":"string"},"fee_month":{"type":"integer","minimum":1,"maximum":12},"last_four":{"type":"string"},"original_open_date":{"type":"string","format":"date"},"is_business":{"type":"boolean"},"counts_toward_524":{"type":"boolean"},"product_changed_from":{"type":"string"},"notes":{"type":"string"}}}}}},"responses":{"201":{"description":"Card created with benefits seeded from the catalog template.","content":{"application/json":{"schema":{"type":"object","properties":{"card_id":{"type":"string"},"card_display_name":{"type":"string"},"matched_template":{"type":"string"},"benefits_created":{"type":"integer"},"fee_month_inferred":{"type":"boolean"},"success":{"type":"boolean"}}}}}},"300":{"description":"Fuzzy name matched multiple templates — pick a card_template_id from candidates.","content":{"application/json":{"schema":{"type":"object","properties":{"ambiguous":{"type":"boolean","const":true},"message":{"type":"string"},"candidates":{"type":"array","items":{"type":"object","properties":{"card_template_id":{"type":"string"},"issuer":{"type":"string"},"name":{"type":"string"},"annual_fee":{"type":"number"},"benefit_count":{"type":"integer"}}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Card template not found (exact id) or no fuzzy matches.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cards/bulk":{"post":{"summary":"Bulk import 1-50 cards with fuzzy matching and welcome offers","description":"Reduces 40+ tool calls to 1-3 for portfolio onboarding. Per-card results carry status created | already_exists | ambiguous | failed (partial success is normal — check results[], not just HTTP status). skip_duplicates defaults true (duplicates report already_exists instead of failed). dry_run: true returns matching + dedup results without creating anything (HTTP 200 instead of 201). Owner names must all resolve or the whole request 400s before any writes.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["cards"],"properties":{"cards":{"type":"array","minItems":1,"maxItems":50,"items":{"type":"object","required":["owner"],"properties":{"name":{"type":"string","description":"Fuzzy template match — either this or card_template_id is required."},"card_template_id":{"type":"string"},"owner":{"type":"string"},"nickname":{"type":"string"},"last_four":{"type":"string"},"original_open_date":{"type":"string","format":"date"},"fee_month":{"type":"integer","minimum":1,"maximum":12},"is_business":{"type":"boolean"},"counts_toward_524":{"type":"boolean"},"product_changed_from":{"type":"string"},"notes":{"type":"string"},"welcome_offer":{"type":"object","required":["bonus_currency","spend_deadline"],"properties":{"bonus_amount":{"type":"number"},"bonus_currency":{"type":"string"},"spend_requirement":{"type":"number"},"spend_deadline":{"type":"string","format":"date"},"current_spend":{"type":"number"},"notes":{"type":"string"}}}}}},"dry_run":{"type":"boolean","default":false},"skip_duplicates":{"type":"boolean","default":true}}}}}},"responses":{"200":{"description":"Dry-run result (only when dry_run: true).","content":{"application/json":{"schema":{"type":"object","properties":{"summary":{"type":"object","properties":{"total":{"type":"integer"},"created":{"type":"integer"},"already_exists":{"type":"integer"},"ambiguous":{"type":"integer"},"failed":{"type":"integer"},"dry_run":{"type":"boolean"}}},"results":{"type":"array","items":{"$ref":"#/components/schemas/BulkCardResult"}}}}}}},"201":{"description":"Import complete (individual cards may still be ambiguous/failed — check results[]).","content":{"application/json":{"schema":{"type":"object","properties":{"summary":{"type":"object","properties":{"total":{"type":"integer"},"created":{"type":"integer"},"already_exists":{"type":"integer"},"ambiguous":{"type":"integer"},"failed":{"type":"integer"},"dry_run":{"type":"boolean"},"periods_warning":{"type":"string"}}},"results":{"type":"array","items":{"$ref":"#/components/schemas/BulkCardResult"}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/cards/{id}":{"get":{"summary":"Single card detail with benefits, ROI, and value at risk","description":"Full card detail: fee-year ROI window, tracked benefits with their current period, retention offer history (structured scalars canonical, legacy 'accepted' kept for compat), recent card events, and the most recent welcome offer. Reduced field set documented here — see additionalProperties.","parameters":[{"name":"id","in":"path","required":true,"description":"UserCard id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Card detail.","content":{"application/json":{"schema":{"type":"object","properties":{"card_id":{"type":"string"},"card_display_name":{"type":"string"},"issuer":{"type":"string"},"issuing_bank":{"type":["string","null"]},"card_name":{"type":"string"},"nickname":{"type":["string","null"]},"owner":{"type":"string"},"annual_fee":{"type":"number"},"renewal_month":{"type":["string","null"]},"notes":{"type":["string","null"]},"original_open_date":{"type":["string","null"],"format":"date"},"product_changed_from":{"type":["string","null"]},"is_business":{"type":"boolean"},"counts_toward_524":{"type":"boolean"},"last_four":{"type":["string","null"]},"added_at":{"type":"string","format":"date"},"roi":{"type":"object","properties":{"captured_value":{"type":"number"},"net_value":{"type":"number"},"roi_percent":{"type":["number","null"]},"fee_year_start":{"type":["string","null"],"format":"date"},"fee_year_end":{"type":["string","null"],"format":"date"}}},"benefits":{"type":"array","items":{"type":"object","properties":{"benefit_id":{"type":"string"},"credit_name":{"type":"string"},"schedule_type":{"type":"string"},"category":{"type":"string"},"current_period":{"type":["object","null"],"properties":{"period_id":{"type":"string"},"period_start":{"type":"string","format":"date"},"period_end":{"type":"string","format":"date"},"amount":{"type":"number"},"amount_used":{"type":"number"},"amount_remaining":{"type":"number"},"unit":{"type":"string"},"days_remaining":{"type":"integer"},"status":{"type":"string"}}}}}},"total_available":{"type":"number"},"total_at_risk_30d":{"type":"number"},"retention_history":{"type":"array","items":{"$ref":"#/components/schemas/RetentionEntry"}},"recent_events":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"},"description":{"type":"string"},"date":{"type":"string","format":"date"}}}},"welcome_offer":{"type":["object","null"],"description":"Most recent welcome offer, reduced shape (no CPP normalization here — use /api/v1/welcome-offers for bonus_value).","additionalProperties":true}},"additionalProperties":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"summary":"Update card metadata and manage retention offers","description":"Multi-purpose: field updates (nickname, notes, open date, 5/24 flags), plus three retention-offer operations that all commit in one transaction. (1) retention_offer logs a new offer — validation: offer_type='points_bonus' requires offer_currency + positive offer_points; 'statement_credit'/'fee_waiver' require offer_value_dollars; offer_points without offer_currency is always rejected; legacy {offer, accepted} without offer_type still passes. When outcome='accepted' AND both spend_requirement + spend_deadline are present, a paired retention-origin WelcomeOffer is auto-created (retention_entry.paired_welcome_offer_id); if pairing was skipped (e.g. an in-progress offer already exists) the additive field retention_entry.pairing_skipped_reason explains why. (2) update_retention_event patches an existing event in place (event_id + date immutable, merged state re-validated, audit breadcrumb in metadata._updates) — response field updated_retention_event matches retention_entry field-for-field. (3) delete_retention_event_id hard-deletes an event AND cascades to its paired WelcomeOffer (cascaded_welcome_offer_id in the response). notes are append-only (timestamped), never replaced.","parameters":[{"name":"id","in":"path","required":true,"description":"UserCard id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"nickname":{"type":"string"},"notes":{"type":"string","description":"Appended with a [YYYY-MM-DD] timestamp, never replaces existing notes."},"original_open_date":{"type":"string","format":"date"},"product_changed_from":{"type":"string"},"is_business":{"type":"boolean"},"counts_toward_524":{"type":"boolean"},"last_four":{"type":"string","pattern":"^\\d{4}$"},"fee_month":{"type":"integer","minimum":1,"maximum":12},"retention_offer":{"type":"object","required":["offer"],"properties":{"date":{"type":"string","format":"date"},"offer":{"type":"string","description":"Free-text offer description."},"accepted":{"type":"boolean","description":"Legacy — prefer 'outcome'."},"notes":{"type":"string"},"offer_type":{"type":"string","enum":["statement_credit","points_bonus","fee_waiver","mq_extension","spend_bonus","other","no_offer"]},"offer_value_dollars":{"type":["number","null"]},"offer_points":{"type":["number","null"]},"offer_currency":{"type":["string","null"]},"outcome":{"type":"string","enum":["accepted","declined","countered","escalated","no_offer"]},"spend_requirement":{"type":"number","minimum":0},"spend_deadline":{"type":"string","format":"date"}}},"delete_retention_event_id":{"type":"string"},"update_retention_event":{"type":"object","required":["event_id"],"properties":{"event_id":{"type":"string"},"offer":{"type":"string"},"offer_type":{"type":"string"},"offer_value_dollars":{"type":["number","null"]},"offer_points":{"type":["number","null"]},"offer_currency":{"type":["string","null"]},"outcome":{"type":"string"},"notes":{"type":"string"}}}}}}}},"responses":{"200":{"description":"Updated card state plus change log and retention results.","content":{"application/json":{"schema":{"type":"object","properties":{"card_id":{"type":"string"},"nickname":{"type":["string","null"]},"notes":{"type":["string","null"]},"original_open_date":{"type":["string","null"],"format":"date"},"product_changed_from":{"type":["string","null"]},"is_business":{"type":"boolean"},"counts_toward_524":{"type":"boolean"},"last_four":{"type":["string","null"]},"fee_month":{"type":["integer","null"]},"changes":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"old_value":{"type":["string","null"]},"new_value":{"type":["string","null"]}}}},"retention_entry":{"oneOf":[{"$ref":"#/components/schemas/RetentionEntry"},{"type":"null"}]},"updated_retention_event":{"oneOf":[{"$ref":"#/components/schemas/RetentionEntry"},{"type":"null"}]},"cascaded_welcome_offer_id":{"type":["string","null"],"description":"Paired WelcomeOffer id deleted by the retention delete cascade."},"success":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Card not found, or retention event not found on this card.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"summary":"Archive a card (soft delete)","description":"Sets deactivatedAt. Idempotent — archiving an already-archived card returns the same 200.","parameters":[{"name":"id","in":"path","required":true,"description":"UserCard id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Card archived.","content":{"application/json":{"schema":{"type":"object","properties":{"card_id":{"type":"string"},"archived":{"type":"boolean"},"success":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/action-items":{"get":{"summary":"Prioritized action items with configurable types and time window","description":"Uniform priority scoring: value_at_risk x urgency_factor, bucketed critical/high/medium/low. action_types accepts BOTH category names (expiring, renewal_approaching, unused_prior_period, negative_roi, welcome_offer_at_risk) AND response-type names (benefit_expiring, at_risk_this_period, fee_renewal) — category names expand to all their response types, response-type names filter strictly. at_risk_this_period replaces benefit_expiring for the same period when the historical capture rate gate fires (never both).","parameters":[{"name":"days_ahead","in":"query","schema":{"type":"integer","minimum":0},"description":"Lookahead window in days. Non-integer or negative returns 400."},{"name":"action_types","in":"query","schema":{"type":"string"},"description":"Comma-separated filter. Valid values: expiring, unused_prior_period, renewal_approaching, negative_roi, welcome_offer_at_risk, benefit_expiring, at_risk_this_period, fee_renewal. Unknown values return 400."}],"responses":{"200":{"description":"Action item queue.","content":{"application/json":{"schema":{"type":"object","properties":{"generated_at":{"type":"string","format":"date-time"},"action_items":{"type":"array","items":{"$ref":"#/components/schemas/ActionItem"}},"total_items":{"type":"integer"},"summary":{"type":"object","properties":{"critical_count":{"type":"integer"},"total_value_at_risk":{"type":"number"},"items_by_type":{"type":"object","additionalProperties":{"type":"integer"}}}},"suppressed_fees":{"type":"array","description":"Fee renewals suppressed because the card is genuinely new with the issuer.","items":{"type":"object","properties":{"card_id":{"type":"string"},"card_display_name":{"type":"string"},"annual_fee":{"type":"number"},"days_since_posted":{"type":"integer"},"reason":{"type":"string","const":"new_card_with_issuer"}}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/calendar-events":{"get":{"summary":"Structured calendar events for agent orchestration","description":"Seven event types across benefits, fees, welcome offers, loyalty programs, and rewards. Default window: today (Pacific-anchored) + 30 days; maximum range 365 days. Benefit periods fan out into multiple reminder events (expiry warnings, availability, mid-period). Retention-origin welcome_offer_deadline events carry origin + source_event_id fields and a '[Retention] ' title prefix.","parameters":[{"name":"start_date","in":"query","schema":{"type":"string","format":"date"},"description":"Defaults to today (Pacific)."},{"name":"end_date","in":"query","schema":{"type":"string","format":"date"},"description":"Defaults to start_date + 30 days. Max 365 days after start_date."},{"name":"event_types","in":"query","schema":{"type":"string"},"description":"Comma-separated subset of: expiry, credit_reset, annual_fee_due, benefit_available, welcome_offer_deadline, loyalty_program_expiry, reward_expiry. Defaults to all."},{"name":"card","in":"query","schema":{"type":"string"},"description":"Partial match on card nickname."},{"name":"include_used","in":"query","schema":{"type":"boolean","default":false},"description":"Include periods already marked used."}],"responses":{"200":{"description":"Events sorted by date ascending.","content":{"application/json":{"schema":{"type":"object","properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/CalendarEvent"}},"event_count":{"type":"integer"},"date_range":{"type":"object","properties":{"start":{"type":"string","format":"date"},"end":{"type":"string","format":"date"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/catalog":{"get":{"summary":"Search card templates by issuer or name","description":"Supports multi-word queries like 'Amex Platinum' (matches across issuer + name). q must be at least 2 characters.","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":2},"description":"Search query (issuer and/or card name)."}],"responses":{"200":{"description":"Matching card templates.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CatalogCard"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/programs":{"get":{"summary":"List loyalty programs with balances, CPP, staleness","description":"Household loyalty programs with balances, user CPP valuations, estimated dollar value, linked cards, and staleness signals. estimated_value = balance x cpp_valuation / 100.","parameters":[{"name":"has_balance","in":"query","schema":{"type":"string","enum":["true"]},"description":"Only programs with balance > 0."},{"name":"type","in":"query","schema":{"type":"string"},"description":"Filter by program type (e.g. bank, airline, hotel)."},{"name":"owner","in":"query","schema":{"type":"string"},"description":"Filter by owner id."},{"name":"stale_days","in":"query","schema":{"type":"integer","minimum":0},"description":"Return only programs not updated in more than N days (also drives is_stale). Default staleness threshold is 30 days when omitted."}],"responses":{"200":{"description":"Loyalty programs.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/LoyaltyProgram"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"summary":"Create or update a loyalty program balance (absolute or delta)","description":"Upsert keyed by program_key. Provide 'balance' (absolute) OR 'delta' (relative), not both. At least one of balance, delta, cpp_valuation, or notes is required. display_name is required only when creating a new program. Negative resulting balances are rejected.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["program_key"],"properties":{"program_key":{"type":"string","description":"e.g. amex_mr, chase_ur, hyatt."},"balance":{"type":"number","description":"Absolute new balance. Mutually exclusive with delta."},"delta":{"type":"number","description":"Relative adjustment (can be negative). Mutually exclusive with balance."},"display_name":{"type":"string","description":"Required when creating a new program."},"program_type":{"type":"string","default":"bank"},"currency":{"type":"string","default":"points"},"cpp_valuation":{"type":"number","minimum":0},"owner":{"type":"string"},"expiration_date":{"type":"string","format":"date"},"expiration_policy":{"type":"string"},"notes":{"type":"string"}}}}}},"responses":{"200":{"description":"Balance update result.","content":{"application/json":{"schema":{"type":"object","properties":{"program_key":{"type":"string"},"previous_balance":{"type":"number"},"new_balance":{"type":"number"},"change":{"type":"number"},"cpp_valuation":{"type":"number"},"estimated_value":{"type":"number"},"last_updated":{"type":"string","format":"date-time"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/programs/transfer-partners":{"get":{"summary":"Transfer partners with ratios for a currency","description":"Static transfer-partner map for a source program (e.g. Amex MR to Hyatt at 1:1). Returns an empty array for unknown or partner-less sources.","parameters":[{"name":"source","in":"query","required":true,"schema":{"type":"string"},"description":"Source program key (e.g. amex_mr, chase_ur)."}],"responses":{"200":{"description":"Transfer partners (empty array when the source has none).","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"target_program_key":{"type":"string"},"target_display_name":{"type":"string"},"ratio":{"type":"number"},"min_transfer":{"type":["integer","null"]},"notes":{"type":["string","null"]}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/earn-rates":{"get":{"summary":"Earn rates for a card (bonus categories, multipliers, caps)","description":"Pass card_id (a card in your portfolio) or card_template_id (any catalog template). CPP valuations come from the household's LoyaltyProgram rows; cpp_valuation and value_per_dollar_cents are null for currencies without one.","parameters":[{"name":"card_id","in":"query","schema":{"type":"string"},"description":"UserCard id — either this or card_template_id is required."},{"name":"card_template_id","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Earn rules sorted by multiplier descending.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"category":{"type":"string"},"multiplier":{"type":"number"},"currency":{"type":"string"},"currency_display_name":{"type":"string"},"cpp_valuation":{"type":["number","null"]},"value_per_dollar_cents":{"type":["number","null"]},"channel":{"type":["string","null"]},"cap_amount":{"type":["number","null"]},"cap_period":{"type":["string","null"]},"is_rotating":{"type":"boolean"},"verified_date":{"type":"string","format":"date"},"notes":{"type":["string","null"]}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/best-card":{"get":{"summary":"Wallet optimizer: best card for a spend category ranked by CPP value","description":"Ranks portfolio cards for a spend category by cents-of-value per dollar. Cards with an in-progress welcome offer are boosted to the top by default (sub_boosted: true + sub_note) — disable with include_sub_priority=false.","parameters":[{"name":"category","in":"query","required":true,"schema":{"type":"string"},"description":"Spend category, e.g. dining, flights, groceries."},{"name":"amount","in":"query","schema":{"type":"number","minimum":0},"description":"Spend amount for points_earned / estimated_value projections."},{"name":"owner","in":"query","schema":{"type":"string"},"description":"Restrict to one owner's cards."},{"name":"channel","in":"query","schema":{"type":"string"},"description":"Purchase channel filter (e.g. portal, direct)."},{"name":"cpp_override","in":"query","schema":{"type":"string"},"description":"Per-currency CPP overrides, format 'amex_mr:2.5,chase_ur:2.0'."},{"name":"include_sub_priority","in":"query","schema":{"type":"string","enum":["true","false"],"default":"true"},"description":"When true (default), cards with active minimum-spend clocks rank first."}],"responses":{"200":{"description":"Ranked cards, best first.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"rank":{"type":"integer"},"card_id":{"type":"string"},"card_display_name":{"type":"string"},"owner":{"type":"string"},"multiplier":{"type":"number"},"currency":{"type":"string"},"currency_display_name":{"type":"string"},"cpp_used":{"type":["number","null"]},"value_per_dollar_cents":{"type":["number","null"]},"points_earned":{"type":["number","null"]},"estimated_value":{"type":["number","null"]},"channel":{"type":["string","null"]},"cap_amount":{"type":["number","null"]},"cap_period":{"type":["string","null"]},"is_rotating":{"type":"boolean"},"notes":{"type":["string","null"]},"sub_boosted":{"type":"boolean"},"sub_note":{"type":["string","null"]}},"additionalProperties":true}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/welcome-offers":{"get":{"summary":"List welcome offers with spend progress and deadline urgency","description":"Defaults to status=in_progress; pass status=all for every status. bonus_value is the bonus normalized to dollars, with bonus_value_source telling you whether the CPP was user-customized ('user'), a market-consensus fallback ('fallback'), or hard cashback ('cashback').","parameters":[{"name":"status","in":"query","schema":{"type":"string","default":"in_progress"},"description":"in_progress (default), earned, missed, cancelled, or 'all'."},{"name":"owner","in":"query","schema":{"type":"string"},"description":"Owner profile name (case-insensitive exact match)."},{"name":"expiring_within_days","in":"query","schema":{"type":"integer","minimum":0},"description":"Only offers with 0 <= days_remaining <= N."},{"name":"origin","in":"query","schema":{"type":"string","enum":["signup","retention"]},"description":"Filter by offer origin; omit for all."}],"responses":{"200":{"description":"Welcome offers sorted by spend_deadline ascending.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WelcomeOffer"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"summary":"Create a welcome offer (bonus, spend requirement, deadline)","description":"One in-progress offer per card — returns 409 if the card already has one. Retention-origin offers are normally created automatically via the retention_offer pairing on PATCH /api/v1/cards/{id}.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["card_id","bonus_amount","bonus_currency","spend_requirement","spend_deadline"],"properties":{"card_id":{"type":"string"},"bonus_amount":{"type":"number","minimum":0},"bonus_currency":{"type":"string"},"spend_requirement":{"type":"number","minimum":0},"spend_deadline":{"type":"string","format":"date"},"current_spend":{"type":"number","minimum":0,"default":0},"status":{"type":"string","default":"in_progress"},"notes":{"type":"string"}}}}}},"responses":{"201":{"description":"Created offer (serialized shape plus success flag).","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WelcomeOffer"},{"type":"object","properties":{"success":{"type":"boolean"}}}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Card already has an in-progress welcome offer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/welcome-offers/{cardId}":{"patch":{"summary":"Update spend progress or status on a card's active welcome offer","description":"Targets the card's most recent in-progress offer (path param is the CARD id, not the offer id). Provide current_spend (absolute) OR spend_delta (relative), not both. Notes are append-only with a timestamp.","parameters":[{"name":"cardId","in":"path","required":true,"description":"UserCard id (the offer is resolved from the card's active in-progress offer).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"current_spend":{"type":"number","minimum":0,"description":"Absolute spend. Mutually exclusive with spend_delta."},"spend_delta":{"type":"number","description":"Relative spend adjustment (may be negative, but resulting spend cannot go below 0)."},"status":{"type":"string","enum":["earned","missed","cancelled"]},"notes":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated offer (serialized shape plus success flag).","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WelcomeOffer"},{"type":"object","properties":{"success":{"type":"boolean"}}}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"No active welcome offer found for this card.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/524-status":{"get":{"summary":"Compute Chase 5/24 eligibility from card metadata + external cards","description":"Counts personal cards opened in the last 24 months (portfolio cards flagged counts_toward_524 plus external cards). Business cards are excluded. warnings lists data-quality issues (e.g. a counting card with no open date).","parameters":[{"name":"owner","in":"query","schema":{"type":"string"},"description":"Restrict the computation to one owner."}],"responses":{"200":{"description":"5/24 status.","content":{"application/json":{"schema":{"type":"object","properties":{"current_count":{"type":"integer"},"limit":{"type":"integer","const":5},"slots_available":{"type":"integer"},"is_eligible":{"type":"boolean"},"cards_counting":{"type":"array","items":{"type":"object","properties":{"card_id":{"type":"string"},"card_display_name":{"type":"string"},"owner":{"type":"string"},"original_open_date":{"type":"string","format":"date"},"falls_off_date":{"type":"string","format":"date"},"days_until_falloff":{"type":"integer"},"is_external":{"type":"boolean"}}}},"next_slot_opens":{"type":["string","null"],"format":"date"},"days_until_next_slot":{"type":["integer","null"]},"cards_excluded":{"type":"array","items":{"type":"object","properties":{"card_id":{"type":"string"},"card_display_name":{"type":"string"},"owner":{"type":"string"},"reason":{"type":"string"}}}},"warnings":{"type":"array","items":{"type":"string"}}},"additionalProperties":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/external-cards":{"get":{"summary":"List external cards (non-catalog cards for 5/24 tracking)","description":"Cards tracked only for Chase 5/24 counting — no benefits or periods. Sorted by open_date descending.","responses":{"200":{"description":"External cards, newest open date first.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"card_name":{"type":"string"},"open_date":{"type":"string","format":"date"},"is_business":{"type":"boolean"},"owner":{"type":["string","null"]},"notes":{"type":["string","null"]},"created_at":{"type":"string","format":"date"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"summary":"Add an external card (card_name, open_date)","description":"For cards not in the CreditSync catalog that still count toward Chase 5/24. is_business defaults false (business cards don't count toward 5/24).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["card_name","open_date"],"properties":{"card_name":{"type":"string"},"open_date":{"type":"string","format":"date","description":"YYYY-MM-DD; impossible dates are rejected."},"is_business":{"type":"boolean","default":false},"owner":{"type":"string","description":"Owner profile name (case-insensitive); silently ignored if not found."},"notes":{"type":"string"}}}}}},"responses":{"201":{"description":"External card created.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"card_name":{"type":"string"},"open_date":{"type":"string","format":"date"},"is_business":{"type":"boolean"},"owner":{"type":["string","null"]},"notes":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/external-cards/{id}":{"delete":{"summary":"Remove an external card","parameters":[{"name":"id","in":"path","required":true,"description":"ExternalCard id.","schema":{"type":"string"}}],"description":"Hard delete. Returns the removed card's id and name.","responses":{"200":{"description":"External card deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"card_name":{"type":"string"},"deleted":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/rewards":{"get":{"summary":"List rewards (standalone credits & certificates)","description":"DEFAULT: status=active. Omitting status returns only active rewards (matches the dashboard); pass status=all to explicitly include used/expired rows. This default changed 2026-07-01 — agents that relied on the old return-everything behavior must opt in with status=all. expiring_within_days implies status=active and filters to rewards with an expiration date within N days (Pacific-anchored today).","parameters":[{"name":"status","in":"query","schema":{"type":"string","default":"active"},"description":"active (default), used, expired, or 'all'."},{"name":"type","in":"query","schema":{"type":"string"},"description":"Filter by reward type (e.g. gift_card, voucher)."},{"name":"expiring_within_days","in":"query","schema":{"type":"integer","minimum":1},"description":"Positive integer; forces status=active and requires a non-null expiration date within the window."}],"responses":{"200":{"description":"Rewards sorted by expiration date (nulls last).","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Reward"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"summary":"Create a reward (name, type, value, expiration_date, source)","description":"Existence-only tracking (no card numbers or redemption codes). Only 'name' is required.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"type":{"type":"string","default":"other"},"value":{"type":"number","minimum":0},"currency":{"type":"string","default":"dollars"},"expiration_date":{"type":"string","format":"date"},"notes":{"type":"string"},"source":{"type":"string"},"owner":{"type":"string","description":"Owner profile name; silently ignored if not found."}}}}}},"responses":{"201":{"description":"Reward created.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"value":{"type":["number","null"]},"currency":{"type":"string"},"expiration_date":{"type":["string","null"],"format":"date"},"status":{"type":"string"},"source":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/rewards/{id}":{"patch":{"summary":"Update a reward (mark used, edit details)","description":"Setting status: 'used' also stamps used_at with the current time.","parameters":[{"name":"id","in":"path","required":true,"description":"Reward id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"value":{"type":["number","null"]},"currency":{"type":"string"},"expiration_date":{"type":["string","null"],"format":"date","description":"Pass null (or empty) to clear the expiration date."},"notes":{"type":["string","null"]},"source":{"type":["string","null"]},"status":{"type":"string","description":"e.g. active, used, expired."}}}}}},"responses":{"200":{"description":"Updated reward.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"value":{"type":["number","null"]},"currency":{"type":"string"},"expiration_date":{"type":["string","null"],"format":"date"},"status":{"type":"string"},"source":{"type":["string","null"]},"notes":{"type":["string","null"]},"used_at":{"type":["string","null"],"format":"date"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"summary":"Remove a reward","parameters":[{"name":"id","in":"path","required":true,"description":"Reward id.","schema":{"type":"string"}}],"description":"Hard delete.","responses":{"200":{"description":"Reward deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"deleted":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}