{
  "openapi": "3.1.0",
  "info": {
    "title": "Affitor Tracking API",
    "version": "1.0.0",
    "description": "Server-to-server tracking API for Affitor advertisers.\n\nThe integration is a three-step funnel:\n1. **Click** — captured automatically by the Affitor tracker script (`?aff=` links), or reported server-side.\n2. **Lead** — ties your internal customer ID (`customer_key`) to the click at signup.\n3. **Sale** — reports revenue; Affitor resolves the partner and computes commission.\n\nRefunds and chargebacks are reported via the Refund endpoint (or handled\nautomatically when using Stripe webhook tracking).\n\n## Authentication\nServer-to-server calls use your **program API key** as a Bearer token:\n`Authorization: Bearer <PROGRAM_API_KEY>`.\nFind it in your advertiser dashboard under Program Settings (shown masked;\nregenerating issues a new key and invalidates the old one immediately).\n\n## Test mode\nThe click, lead and sale endpoints accept `additional_data: { \"test_mode\": true }`\nto create isolated test events (`is_test: true`) that never touch production\nattribution or commissions and are left out of revenue analytics; they can\nmove your setup checklist. `POST /api/v1/track/refund` has no test mode:\nwith your program API key it is processed as a real refund.\nFull guide: https://affitor.com/docs/api-reference/test-mode\n\n## Attribution rules\n- Model: first click wins inside a program. When a later click from\n  another partner sends the stored `click_id` as `existing_click_id` (the\n  tracker does), that click is recorded for the new partner, but the\n  customer, its `click_id` and its partner do not change.\n- Attribution window: program-level `attribution_window_days` (default 60).\n  Sales for customers whose first-click anchor is outside the window return\n  `attributed: false` and create nothing.\n- Customer lifecycle: `click → lead → conversion` (status never regresses).\n",
    "contact": {
      "name": "Affitor Support",
      "url": "https://affitor.com/docs/support/contact"
    }
  },
  "servers": [
    {
      "url": "https://api.affitor.com",
      "description": "Production"
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "Tracking",
      "description": "Click, lead, sale, and refund tracking"
    },
    {
      "name": "Agent",
      "description": "Agent-facing verification surface — synthetic chain + readiness polling"
    }
  ],
  "paths": {
    "/api/v1/track/click": {
      "post": {
        "operationId": "trackClick",
        "tags": [
          "Tracking"
        ],
        "summary": "Track a click",
        "description": "Records an affiliate click. Normally called by the Affitor tracker\nscript when a visitor lands with `?aff=<partner-code>`; can also be\ncalled server-side. No authentication required — attribution is proven\nby the `aff` short code inside `affiliate_url`.\n\nOn success, store the returned `click_id` in a first-party cookie named\n`affitor_click_id` with a lifetime of `cookie_window_days` days.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClickRequest"
              },
              "examples": {
                "production": {
                  "summary": "Real click",
                  "value": {
                    "affiliate_url": "https://yoursite.com/pricing?aff=abc123",
                    "page_url": "https://yoursite.com/pricing?aff=abc123",
                    "page_title": "Pricing",
                    "referrer_url": "https://google.com",
                    "session_id": "sess_xyz_1234"
                  }
                },
                "test_mode": {
                  "summary": "Test mode",
                  "value": {
                    "additional_data": {
                      "test_mode": true,
                      "program_id": "YOUR_PROGRAM_ID"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "description": "Click recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ClickResponse"
                    },
                    {
                      "$ref": "#/components/schemas/TestModeResponse"
                    }
                  ]
                },
                "examples": {
                  "recorded": {
                    "value": {
                      "success": true,
                      "click_id": "cust_42_1714000000000",
                      "partner_code": "abc123",
                      "cookie_window_days": 60
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error. Causes: missing `affiliate_url`, URL not\nparseable, no `aff` parameter present, a `program_id` that is not a\nprogram (`Unknown program_id`), referral link not found, or invalid\nreferral link configuration.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Test mode only: the Bearer key and `additional_data.program_id`\nname different programs (`program_id does not match the API key`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "data": null,
                  "error": {
                    "status": 403,
                    "name": "ForbiddenError",
                    "message": "program_id does not match the API key",
                    "details": {}
                  }
                }
              }
            }
          },
          "410": {
            "description": "The referral link exists but is inactive (turned off by the partner\nor brand). No click was recorded. The body also carries\n`success: false` and `message` for older clients.\n",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/AgentError"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "success": {
                          "type": "boolean",
                          "example": false
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                },
                "examples": {
                  "link_disabled": {
                    "value": {
                      "success": false,
                      "message": "Referral link is inactive",
                      "error": {
                        "code": "link_disabled",
                        "message": "Referral link is inactive",
                        "hint": "The partner or brand turned this link off. No click was recorded."
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/track/lead": {
      "post": {
        "operationId": "trackLead",
        "tags": [
          "Tracking"
        ],
        "summary": "Track a lead (signup)",
        "description": "Ties a signup to a prior click. Two modes:\n\n- **Server mode (recommended)** — send the Bearer program API key and\n  your internal user ID as `customer_key`. Read the `affitor_click_id`\n  cookie server-side and pass it as `click_id`.\n- **Browser mode** — called by `window.affitor.signup(customerKey, email)`\n  with no Bearer token; the click cookie proves attribution. Programs\n  configured with `lead_auth: server_required` (every program unless it\n  is set to `browser_allowed`) record browser leads as **provisional**\n  only. Attribution settles when your backend sends the server-side\n  lead, or when a Stripe Checkout payment for that customer arrives\n  through your connected Stripe account. A sale sent to\n  `/api/v1/track/sale` does not settle it.\n\nSend the `click_id` with your `customer_key`. A request with neither\nanswers `400` (`click_id or customer_key is required`). A first lead\nwith only `customer_key` answers `400` (`Customer not found. Provide a\nvalid click_id or customer_key.`), because Affitor saves your key on\nthe customer only when a lead settles. Outside test mode and without a\nBearer key, `click_id` is required. The email, when provided, is hashed\nserver-side before storage.\n",
        "security": [
          {
            "bearerAuth": []
          },
          {}
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LeadRequest"
              },
              "examples": {
                "server_mode": {
                  "summary": "Server mode (Bearer)",
                  "value": {
                    "click_id": "cust_42_1714000000000",
                    "customer_key": "user_123",
                    "email": "user@example.com"
                  }
                },
                "test_mode": {
                  "summary": "Test mode",
                  "value": {
                    "click_id": "test_lead_001",
                    "customer_key": "test_customer",
                    "additional_data": {
                      "test_mode": true,
                      "program_id": "YOUR_PROGRAM_ID"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "description": "Lead recorded (or recorded as provisional).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/LeadResponse"
                    },
                    {
                      "$ref": "#/components/schemas/LeadProvisionalResponse"
                    },
                    {
                      "$ref": "#/components/schemas/TestModeResponse"
                    }
                  ]
                },
                "examples": {
                  "tracked": {
                    "value": {
                      "success": true,
                      "message": "Lead tracked successfully"
                    }
                  },
                  "provisional": {
                    "value": {
                      "success": true,
                      "provisional": true,
                      "message": "Lead recorded as provisional. This program requires server-side (Bearer) lead tracking to settle attribution."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error. Causes: neither `click_id` nor `customer_key`\nprovided; a `customer_key` without `click_id` and without a Bearer\nkey (`click_id is required when no API key is sent`); customer not\nfound, including a first lead sent with only `customer_key` and, in\nbrowser mode, a customer that does not belong to the `program_id`\nsent; customer belongs to a different program than\nthe authenticated one; customer status cannot transition to lead.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Test mode only: the Bearer key and `additional_data.program_id`\nname different programs (`program_id does not match the API key`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "data": null,
                  "error": {
                    "status": 403,
                    "name": "ForbiddenError",
                    "message": "program_id does not match the API key",
                    "details": {}
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/track/sale": {
      "post": {
        "operationId": "trackSale",
        "tags": [
          "Tracking"
        ],
        "summary": "Track a sale",
        "description": "Reports a payment. Affitor resolves the partner from the customer's\nclick→lead attribution and creates the commission — do not send a\npartner code or a pre-computed commission amount.\n\n`transaction_id` is the **idempotency key** (unique per platform): use\nyour payment processor's invoice/charge ID. A duplicate returns HTTP\n409 — treat 409 as already-recorded, not as a retryable failure.\n\nIf the customer's attribution window has expired, the call succeeds\nwith `attributed: false` and creates no sale or commission (the sale is\norganic).\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SaleRequest"
              },
              "examples": {
                "one_time": {
                  "summary": "One-time payment",
                  "value": {
                    "transaction_id": "txn_abc123",
                    "customer_key": "user_123",
                    "amount_cents": 9999,
                    "currency": "USD",
                    "sale_type": "payment"
                  }
                },
                "subscription": {
                  "summary": "Subscription renewal",
                  "value": {
                    "transaction_id": "in_1QxYz...",
                    "customer_key": "user_123",
                    "amount_cents": 2490,
                    "currency": "USD",
                    "sale_type": "subscription",
                    "is_recurring": true,
                    "subscription_id": "sub_xyz",
                    "subscription_interval": "monthly"
                  }
                },
                "test_mode": {
                  "summary": "Test mode",
                  "value": {
                    "additional_data": {
                      "test_mode": true
                    },
                    "amount_cents": 9999,
                    "currency": "USD",
                    "sale_type": "payment"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "description": "Sale processed. Either attributed (sale + commission created) or\nexplicitly not attributed (`attributed: false`, window expired).\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/SaleResponse"
                    },
                    {
                      "$ref": "#/components/schemas/SaleNotAttributedResponse"
                    },
                    {
                      "$ref": "#/components/schemas/TestModeResponse"
                    }
                  ]
                },
                "examples": {
                  "attributed": {
                    "value": {
                      "success": true,
                      "sale_id": 123,
                      "commission_id": 456,
                      "message": "Sale tracked successfully"
                    }
                  },
                  "window_expired": {
                    "value": {
                      "success": true,
                      "attributed": false,
                      "reason": "attribution_window_expired",
                      "click_age_days": 75,
                      "window_days": 60,
                      "sale_id": null,
                      "commission_id": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error. Causes: missing/invalid `transaction_id`;\n`amount_cents` not a positive number; customer not found; customer\nhas no attribution in this program; no partner-program\nrelationship.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "Duplicate `transaction_id`. A sale with this id was already recorded. `details.sale_id` is your program's earlier sale id, or `null` when the earlier sale belongs to another program.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "data": null,
                  "error": {
                    "status": 409,
                    "name": "ConflictError",
                    "message": "Duplicate transaction_id. This sale has already been recorded.",
                    "details": {
                      "sale_id": 123
                    },
                    "code": "duplicate_transaction"
                  }
                }
              }
            }
          },
          "422": {
            "description": "The `currency` is not supported. No sale, commission or fee was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "data": null,
                  "error": {
                    "status": 422,
                    "name": "UnprocessableEntityError",
                    "message": "currency XYZ is not supported",
                    "details": {},
                    "code": "unsupported_currency"
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/track/refund": {
      "post": {
        "operationId": "trackRefund",
        "tags": [
          "Tracking"
        ],
        "summary": "Track a refund or chargeback",
        "description": "Reverses the commission for a previously tracked sale. Keyed by the\n**original sale's** `transaction_id` (not the refund event's ID).\n\n- Send one call per refund. `refund_amount_cents` is THIS refund's\n  amount; it is added to what was already refunded (capped at the sale).\n- Omitted `refund_amount_cents` — the rest of the sale is refunded and\n  the commission is reversed.\n- Idempotent per `refund_id`; without it, a repeat of the same amount\n  is treated as a retry and ignored.\n\nWhen using Stripe webhook tracking, `charge.refunded` events trigger\nthe same reversal automatically — no API call needed.\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RefundRequest"
              },
              "example": {
                "transaction_id": "txn_abc123",
                "refund_amount_cents": 5000,
                "refund_id": "re_123",
                "refund_reason": "customer_request"
              }
            }
          }
        },
        "responses": {
          "200": {
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "description": "Refund processed (`status: reversed` or `refunded`), nothing\nchanged (`status: skipped`, with a `reason`), or sale found but\nhad no commission to reverse (`status: no_commission`). A test key\nanswers `{ success: true, test: true }` before the sale lookup and\nchanges nothing.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RefundResponse"
                },
                "examples": {
                  "reversed": {
                    "value": {
                      "success": true,
                      "status": "reversed",
                      "deductionAmount": 25,
                      "commission_id": 456
                    }
                  },
                  "skipped": {
                    "value": {
                      "success": true,
                      "status": "skipped",
                      "reason": "already reversed",
                      "commission_id": 456
                    }
                  },
                  "no_commission": {
                    "value": {
                      "success": true,
                      "status": "no_commission",
                      "message": "Sale has no commission to reverse"
                    }
                  },
                  "test_key": {
                    "value": {
                      "success": true,
                      "test": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`transaction_id` is required, or `reason` is not `refund` or `chargeback` (`reason must be refund or chargeback`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No sale matches `transaction_id` for your program.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "data": null,
                  "error": {
                    "status": 404,
                    "name": "NotFoundError",
                    "message": "Sale not found for transaction_id",
                    "details": {},
                    "code": "not_found"
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/cli/test-event": {
      "post": {
        "operationId": "testEvent",
        "tags": [
          "Agent"
        ],
        "summary": "Fire a synthetic verification chain",
        "description": "Runs a synthetic **click → lead → sale** through the real attribution +\ncommission pipeline so an integration can be verified without a real\ncustomer or real money. Every row created is `is_test: true` and is\nexcluded from earnings, analytics, payouts, and the readiness `live`\ngate's real-event signal.\n\nSend `{ \"type\": \"chain\" }`. The program is derived from the Bearer\nprogram API key. Rate limited to **10 chains / program / hour** (HTTP\n429 with `Retry-After`).\n\nAfter a successful chain, poll `GET /api/v1/programs/me/readiness` until\n`integration_verified: true`.\n\nA legacy single-event shape (`{ \"event_type\": \"click\" | \"lead\" |\n\"sale\" }`) also exists; it is a dumb insert (always `attributed:false`,\nno commission) and does **not** verify integration — prefer the chain.\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TestEventRequest"
              },
              "examples": {
                "chain": {
                  "summary": "Synthetic verification chain (recommended)",
                  "value": {
                    "type": "chain"
                  }
                },
                "legacy_single": {
                  "summary": "Legacy single dumb-insert event",
                  "value": {
                    "event_type": "click"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "description": "Chain executed (or legacy single event recorded).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TestEventChainResponse"
                },
                "example": {
                  "data": {
                    "type": "chain",
                    "short_code": "afftest42",
                    "customer_key": "aff_test_ck_42_1714000000000_ab12cd",
                    "verdict": {
                      "click": "attributed",
                      "lead": "attributed",
                      "sale": "attributed"
                    },
                    "attributed": true
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid `event_type` (legacy mode) or malformed body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "description": "Rate limited. Two separate caps reach this route: the per-minute\nrequest limit on `/api/v1/cli/*` (60/min per API key), and the\nsynthetic-chain cap of 10 runs per program per hour, shown below.\nBoth report `rate_limited`. Respect the `Retry-After` header and\n`retry_after_seconds`.\n",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait before retrying."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentError"
                },
                "example": {
                  "error": {
                    "status": 429,
                    "code": "rate_limited",
                    "message": "Too many synthetic chains (max 10/program/hour).",
                    "retry_after_seconds": 1800
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/programs/me/readiness": {
      "get": {
        "operationId": "getMyReadiness",
        "tags": [
          "Agent"
        ],
        "summary": "Program readiness (6-gate verdict)",
        "description": "Returns a machine-verifiable readiness verdict for the program behind\nthe Bearer program API key (the program is derived from the key — there\nis no `:id` in the path, so no IDOR and the agent never needs to know\nits own program id).\n\nSix gates, each `pass | fail | unknown`. Four decide completion, in\nthis order: `profile · economics · tracking · live`. `blocker` is the\nfirst of these four that has not passed. Two more are reported but\nnever become `blocker`: `payout` (the advertiser's Stripe Connect,\nfor billing) and `payments` (the program can record sales: its Stripe\naccount is connected, or your server has sent a sale to\n`/api/v1/track/sale`; going live requires it). Each failing gate\ncarries a `next_action` string the agent can act on. Poll every\n`poll.retry_after_seconds` (default 5) until\n`integration_verified: true`.\n\nREAD-ONLY: this endpoint writes nothing. Errors use the typed agent\nenvelope `{ error: { code, message, retry_after_seconds? } }`.\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "force_recheck",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Bypass the tracking-gate cache TTL (itself limited to 1 forced fetch / program / min)."
          }
        ],
        "responses": {
          "200": {
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              }
            },
            "description": "Readiness verdict.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReadinessResponse"
                },
                "example": {
                  "program_id": 42,
                  "state": "integration_verified",
                  "integration_verified": true,
                  "blocker": null,
                  "gates": {
                    "profile": {
                      "status": "pass"
                    },
                    "economics": {
                      "status": "pass"
                    },
                    "payout": {
                      "status": "pass",
                      "mode": "stripe"
                    },
                    "payments": {
                      "status": "pass",
                      "method": "stripe_connect"
                    },
                    "tracking": {
                      "status": "pass",
                      "detail": "tracker_installed",
                      "checked_at": "2026-06-11T00:00:00.000Z"
                    },
                    "live": {
                      "status": "pass",
                      "test_chain": {
                        "click": "attributed",
                        "lead": "attributed",
                        "sale": "attributed"
                      }
                    }
                  },
                  "last_event_at": {
                    "click": null,
                    "lead": null,
                    "sale": null
                  },
                  "counts_24h": {
                    "clicks": 0,
                    "leads": 0,
                    "sales": 0
                  },
                  "key_rotated_at": null,
                  "poll": {
                    "retry_after_seconds": 5
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing/malformed Authorization header, or invalid API token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentError"
                },
                "example": {
                  "error": {
                    "code": "invalid_key",
                    "message": "Invalid API token."
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Failed to compute readiness — back off and retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentError"
                },
                "example": {
                  "error": {
                    "code": "internal_error",
                    "message": "Failed to compute readiness. Retry shortly.",
                    "retry_after_seconds": 10
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Program API key from your advertiser dashboard (Program Settings). A program API key belongs to one program and authorizes every call in this spec that requires it, for that program only."
      }
    },
    "headers": {
      "RateLimitPolicy": {
        "description": "The quota of this route's bucket, as an IETF RateLimit-Policy structured field: `\"default\";q=<requests>;w=60` (requests per 60-second window).",
        "schema": {
          "type": "string",
          "example": "\"default\";q=120;w=60"
        }
      },
      "RateLimit": {
        "description": "What is left of that quota, as an IETF RateLimit structured field: `\"default\";r=<remaining>;t=<seconds until the window resets>`.",
        "schema": {
          "type": "string",
          "example": "\"default\";r=119;t=60"
        }
      },
      "XRateLimitLimit": {
        "description": "Requests allowed per minute in this bucket (legacy header, same value as `q`).",
        "schema": {
          "type": "integer"
        }
      },
      "XRateLimitRemaining": {
        "description": "Requests left in the current window (legacy header, same value as `r`).",
        "schema": {
          "type": "integer"
        }
      },
      "XRateLimitReset": {
        "description": "Unix timestamp in seconds when the window resets (legacy header).",
        "schema": {
          "type": "integer"
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing/malformed Authorization header, or invalid API token.\n\nTwo bodies are possible. A key that simply does not match a program is\nrefused with the platform `Error` envelope. A key that is past its\nrotation deadline is refused with `AgentError` carrying\n`error.code: api_key_rotation_required` — branch on `error.code`.\n",
        "content": {
          "application/json": {
            "schema": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/Error"
                },
                {
                  "$ref": "#/components/schemas/AgentError"
                }
              ]
            },
            "examples": {
              "invalid_token": {
                "summary": "Key matches no program",
                "value": {
                  "data": null,
                  "error": {
                    "status": 401,
                    "name": "UnauthorizedError",
                    "message": "Invalid API token",
                    "details": {}
                  }
                }
              },
              "key_retired": {
                "summary": "Key is past its rotation deadline",
                "value": {
                  "error": {
                    "code": "api_key_rotation_required",
                    "message": "This API key was retired on 2026-10-03.",
                    "hint": "Create a new key in Affitor → Settings → API Key.",
                    "docs_url": "https://affitor.com/docs/api-reference/errors#api_key_rotation_required"
                  }
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Per-minute rate limit exceeded. Refused before the endpoint runs, so\nnothing was recorded and the retry is safe once the window resets.\n",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds to wait before retrying."
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/XRateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/XRateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/XRateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/AgentError"
            },
            "example": {
              "error": {
                "status": 429,
                "code": "rate_limited",
                "message": "Too many requests.",
                "retry_after_seconds": 42
              }
            }
          }
        }
      },
      "InternalError": {
        "description": "Unexpected server error. Safe to retry with the same idempotency key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "The platform envelope: validation, auth and conflict errors raised by\nthe `/api/v1/track/*` and `/api/v1/cli/*` endpoints themselves.\n\n`error` is an OBJECT, not a string: interpolating `body.error` into a\nmessage prints `[object Object]`. Read `error.message`.\n\nIt is not the only envelope these endpoints return. Rate limiting, a key\npast its rotation deadline, and every error on\n`/api/v1/programs/me/readiness` answer with `AgentError` instead — so\nbranch on `error.code` first and fall back to `error.message` when it is\nabsent.\n",
        "properties": {
          "data": {
            "type": "null",
            "description": "Always `null` on an error."
          },
          "error": {
            "type": "object",
            "properties": {
              "status": {
                "type": "integer",
                "description": "Repeats the HTTP status code."
              },
              "name": {
                "type": "string",
                "description": "Error class name for that status: BadRequestError (400), UnauthorizedError (401), NotFoundError (404), ConflictError (409), UnprocessableEntityError (422), InternalServerError (500)."
              },
              "message": {
                "type": "string",
                "description": "Human-readable error message."
              },
              "details": {
                "type": "object",
                "description": "Empty object, except `sale_id` (integer or null) on a track/sale 409."
              },
              "code": {
                "type": "string",
                "enum": [
                  "invalid_request",
                  "no_attribution",
                  "unauthorized",
                  "not_found",
                  "duplicate_transaction",
                  "unsupported_currency",
                  "internal_error"
                ],
                "description": "Stable machine code. Sent by `/api/v1/track/sale` and `/api/v1/track/refund`."
              }
            },
            "required": [
              "status",
              "name",
              "message"
            ]
          }
        },
        "required": [
          "data",
          "error"
        ]
      },
      "TestModeResponse": {
        "type": "object",
        "description": "Returned when `additional_data.test_mode` is true.",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "message": {
            "type": "string"
          },
          "data": {
            "type": "object",
            "properties": {
              "eventId": {
                "type": "integer"
              },
              "programId": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "test_mode": {
                "type": "boolean",
                "const": true
              }
            }
          }
        }
      },
      "AdditionalData": {
        "type": "object",
        "description": "Extension object. Set `test_mode: true` to create an isolated test event.",
        "properties": {
          "test_mode": {
            "type": "boolean"
          },
          "program_id": {
            "type": [
              "string",
              "integer"
            ],
            "description": "Required with `test_mode` on unauthenticated calls so the test event can be linked to your program."
          },
          "event_name": {
            "type": "string",
            "description": "Optional label for the event (lead tracking)."
          }
        }
      },
      "ClickRequest": {
        "type": "object",
        "properties": {
          "affiliate_url": {
            "type": "string",
            "format": "uri",
            "description": "The full landing URL containing `?aff=<partner-short-code>`. Required (except in test mode)."
          },
          "page_url": {
            "type": "string",
            "format": "uri"
          },
          "page_title": {
            "type": "string"
          },
          "referrer_url": {
            "type": "string"
          },
          "session_id": {
            "type": "string"
          },
          "user_agent": {
            "type": "string",
            "description": "Falls back to the request's User-Agent header."
          },
          "screen_resolution": {
            "type": "string",
            "example": "1920x1080"
          },
          "viewport_size": {
            "type": "string",
            "example": "1440x900"
          },
          "language": {
            "type": "string",
            "example": "en-US"
          },
          "timezone": {
            "type": "string",
            "example": "America/New_York"
          },
          "existing_click_id": {
            "type": "string",
            "description": "Pass the stored click ID when the same visitor clicks again. A customer that already has this click ID is reused and keeps its first partner (first click wins inside a program); the click is still recorded for the new partner. With `program_id`, a click ID whose customer belongs to another program is not reused, and the click gets a new click ID and customer."
          },
          "program_id": {
            "type": [
              "string",
              "integer"
            ],
            "description": "The program of the page (the tracker sends it). When sent, the `aff` code is looked up only inside this program, and an id that is not a program returns `400` `Unknown program_id`. Without it, the lookup covers every program."
          },
          "visitor_ip": {
            "type": "string",
            "description": "Your visitor's IP address, when your server records the click for that visitor. Used for the click's location only when the request carries the Bearer API key of the program the link belongs to (not a key past its rotation deadline) and the value is an IP address; otherwise the request's own IP is used. It never causes the click to be refused."
          },
          "additional_data": {
            "$ref": "#/components/schemas/AdditionalData"
          }
        }
      },
      "ClickResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "click_id": {
            "type": "string",
            "description": "Store this in the `affitor_click_id` first-party cookie.",
            "example": "cust_42_1714000000000"
          },
          "partner_code": {
            "type": "string"
          },
          "cookie_window_days": {
            "type": "integer",
            "description": "Cookie lifetime in days. Follows the program's attribution window (default 60)."
          }
        }
      },
      "LeadRequest": {
        "type": "object",
        "description": "Send the `click_id` with your `customer_key`. A first lead with only `customer_key` answers `400` (`Customer not found. Provide a valid click_id or customer_key.`), because Affitor saves your key on the customer only when a lead settles. Outside test mode and without a Bearer key, `click_id` is required.",
        "properties": {
          "click_id": {
            "type": "string",
            "description": "Value of the `affitor_click_id` cookie (read server-side)."
          },
          "customer_key": {
            "type": "string",
            "description": "Your internal, stable user ID. The join key for all future sales."
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Optional. Hashed server-side before storage; never stored in plain text."
          },
          "session_id": {
            "type": "string"
          },
          "page_url": {
            "type": "string"
          },
          "click_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Optional list of candidate click IDs from the tracker SDK (shadow attribution lane)."
          },
          "program_id": {
            "type": [
              "string",
              "integer"
            ],
            "description": "The program of the page (the tracker sends it). Read only in browser mode (no Bearer key); with a key, the key's program decides. When sent, the customer found by `click_id` or `customer_key` must belong to this program; otherwise the lead answers `400` `Customer not found. Provide a valid click_id or customer_key.`"
          },
          "additional_data": {
            "$ref": "#/components/schemas/AdditionalData"
          }
        }
      },
      "LeadResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "message": {
            "type": "string",
            "example": "Lead tracked successfully"
          }
        }
      },
      "LeadProvisionalResponse": {
        "type": "object",
        "description": "Browser-mode lead on a program with `lead_auth: server_required`.",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "provisional": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string"
          }
        }
      },
      "SaleRequest": {
        "type": "object",
        "required": [
          "transaction_id",
          "amount_cents"
        ],
        "properties": {
          "transaction_id": {
            "type": "string",
            "description": "Idempotency key — unique per sale. Use your payment processor's invoice/charge ID. Duplicates return HTTP 409."
          },
          "customer_key": {
            "type": "string",
            "description": "Your internal user ID (preferred lookup). Provide this or `click_id`."
          },
          "click_id": {
            "type": "string",
            "description": "Alternative lookup via the `affitor_click_id` cookie value."
          },
          "amount_cents": {
            "type": "integer",
            "minimum": 1,
            "description": "Gross sale amount in cents."
          },
          "tax_cents": {
            "type": "integer",
            "minimum": 0,
            "description": "The tax included in `amount_cents`, same unit. Accepted but not used yet; commissions are calculated on `amount_cents`."
          },
          "currency": {
            "type": "string",
            "default": "USD",
            "enum": [
              "USD",
              "EUR",
              "VND"
            ],
            "description": "Another code → 422 `unsupported_currency`; no sale, commission or fee is created. Amounts are not converted."
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the payment happened (ISO 8601 with offset, at most 30 days old and 5 minutes ahead). Stored for reporting only."
          },
          "provider": {
            "type": "string",
            "enum": [
              "stripe",
              "polar",
              "dodo",
              "paddle",
              "lemonsqueezy",
              "creem",
              "sepay",
              "payos",
              "apipay",
              "x402",
              "other"
            ],
            "description": "Your payment provider. Stored for reporting."
          },
          "provider_payment_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "description": "The provider's payment id. Stored for reporting."
          },
          "livemode": {
            "type": "boolean",
            "description": "`false` records a test sale (same as `additional_data.test_mode: true`). `true` does not override `test_mode` or a test key."
          },
          "sale_type": {
            "type": "string",
            "enum": [
              "payment",
              "subscription"
            ],
            "default": "payment"
          },
          "is_recurring": {
            "type": "boolean",
            "default": false,
            "description": "True for subscription renewals."
          },
          "subscription_id": {
            "type": "string"
          },
          "subscription_interval": {
            "type": "string",
            "enum": [
              "monthly",
              "quarterly",
              "annual"
            ]
          },
          "product_id": {
            "type": "string",
            "description": "Optional product identifier, used for per-product commission policies."
          },
          "line_items": {
            "description": "Optional structured line items (JSON)."
          },
          "additional_data": {
            "$ref": "#/components/schemas/AdditionalData"
          }
        }
      },
      "SaleResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "sale_id": {
            "type": "integer"
          },
          "commission_id": {
            "type": "integer"
          },
          "message": {
            "type": "string",
            "example": "Sale tracked successfully"
          }
        }
      },
      "SaleNotAttributedResponse": {
        "type": "object",
        "description": "The customer's first-click anchor is outside the attribution window — the sale is organic. Nothing is created.",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "attributed": {
            "type": "boolean",
            "const": false
          },
          "reason": {
            "type": "string",
            "example": "attribution_window_expired"
          },
          "click_age_days": {
            "type": "integer"
          },
          "window_days": {
            "type": "integer"
          },
          "sale_id": {
            "type": "null"
          },
          "commission_id": {
            "type": "null"
          }
        }
      },
      "RefundRequest": {
        "type": "object",
        "required": [
          "transaction_id"
        ],
        "properties": {
          "transaction_id": {
            "type": "string",
            "description": "The ORIGINAL sale's `transaction_id` (not the refund event's ID)."
          },
          "refund_amount_cents": {
            "type": "integer",
            "minimum": 0,
            "description": "This refund's amount (not the running total), in the same unit as the sale's `amount_cents`. Omit or send 0 to refund the rest of the sale."
          },
          "refund_id": {
            "type": "string",
            "description": "Your id for this refund (e.g. the Stripe refund id). A refund_id already applied is ignored. Not used when refund_amount_cents is omitted or 0."
          },
          "refund_reason": {
            "type": "string",
            "description": "Free-text reason, stored for audit."
          },
          "reason": {
            "type": "string",
            "enum": [
              "refund",
              "chargeback"
            ],
            "description": "Stored as the reason when `refund_reason` is not sent or empty."
          }
        }
      },
      "RefundResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "commission_id": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "reversed",
              "refunded",
              "skipped",
              "no_commission"
            ],
            "description": "`reversed` once the whole sale is refunded, `refunded` while part of it remains, `skipped` when the call changed nothing (`reason` says why), `no_commission` when the sale had no commission to reverse."
          },
          "deductionAmount": {
            "type": "number",
            "description": "The commission share this refund covers, in the commission's currency: the refunded fraction of the commission, or all of what is left once the sale is fully refunded. Taken from the partner's balance only when the commission was already credited (past `pending`, not a test). A `pending` commission that is partly refunded is cancelled whole, so for it this number is informational. Present when `status` is `reversed` or `refunded`."
          },
          "reason": {
            "type": "string",
            "description": "Why the call changed nothing. Present when `status` is `skipped`, e.g. `already reversed` or `refund already applied`."
          },
          "message": {
            "type": "string",
            "description": "Present with `status: no_commission`."
          },
          "test": {
            "type": "boolean",
            "const": true,
            "description": "Present (and the only other field) when the request used a test key. Nothing was refunded."
          }
        }
      },
      "AgentError": {
        "type": "object",
        "description": "Typed agent error envelope so an agent can branch on `code`. No `data`\nkey. Returned by rate limiting on every `/api/v1/` route, by a key past\nits rotation deadline, by every error on the readiness endpoint, and by\n`POST /api/v1/track/click` on an inactive referral link (`link_disabled`).\n\nOnly `code` and `message` are always present; the rest depends on the\ncase.\n",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine code (e.g. invalid_key, rate_limited, api_key_rotation_required, link_disabled, internal_error)."
              },
              "message": {
                "type": "string"
              },
              "status": {
                "type": "integer",
                "description": "Repeats the HTTP status. Sent on rate-limit errors."
              },
              "retry_after_seconds": {
                "type": "integer",
                "description": "Present on retryable errors (e.g. rate limits, transient internal errors)."
              },
              "hint": {
                "type": "string",
                "description": "What to do instead of retrying. Sent with api_key_rotation_required."
              },
              "docs_url": {
                "type": "string",
                "format": "uri",
                "description": "Page explaining this code. Sent with api_key_rotation_required."
              }
            },
            "required": [
              "code",
              "message"
            ]
          }
        },
        "required": [
          "error"
        ]
      },
      "TestEventRequest": {
        "type": "object",
        "description": "Send `{ \"type\": \"chain\" }` to run the synthetic verification chain\n(recommended). The legacy single-event shape (`event_type`) is a dumb\ninsert that does not verify integration.\n",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "chain"
            ],
            "description": "Run a synthetic click→lead→sale chain through the real pipeline."
          },
          "event_type": {
            "type": "string",
            "enum": [
              "click",
              "lead",
              "sale"
            ],
            "description": "Legacy single dumb-insert event (always unattributed, no commission)."
          }
        }
      },
      "TestEventChainResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "const": "chain"
              },
              "short_code": {
                "type": "string",
                "description": "The synthetic referral short code used for this program's fixture."
              },
              "customer_key": {
                "type": "string",
                "description": "The synthetic customer key threaded through all three steps."
              },
              "verdict": {
                "$ref": "#/components/schemas/TestChainVerdict"
              },
              "attributed": {
                "type": "boolean",
                "description": "True when the synthetic sale attributed (the chain's terminal success)."
              }
            }
          }
        }
      },
      "TestChainVerdict": {
        "type": "object",
        "description": "Per-step verdict for the synthetic chain.",
        "properties": {
          "click": {
            "$ref": "#/components/schemas/TestChainStepStatus"
          },
          "lead": {
            "$ref": "#/components/schemas/TestChainStepStatus"
          },
          "sale": {
            "$ref": "#/components/schemas/TestChainStepStatus"
          }
        }
      },
      "TestChainStepStatus": {
        "type": "string",
        "enum": [
          "attributed",
          "unattributed",
          "wrong_partner",
          "pending"
        ]
      },
      "GateStatus": {
        "type": "string",
        "enum": [
          "pass",
          "fail",
          "unknown"
        ]
      },
      "ReadinessResponse": {
        "type": "object",
        "description": "Machine-verifiable readiness verdict. Poll until\n`integration_verified: true`. READ-ONLY.\n",
        "properties": {
          "program_id": {
            "type": "integer"
          },
          "state": {
            "type": "string",
            "enum": [
              "draft",
              "configuring",
              "ready",
              "integration_verified",
              "live_verified"
            ],
            "description": "State machine. `integration_verified` = the four completion gates\npass, `live` through the synthetic chain (no real event yet): the\nagent's goal. `live_verified` = a real (non-test) event has flowed.\n"
          },
          "integration_verified": {
            "type": "boolean",
            "description": "True when `state` is `integration_verified` or `live_verified`."
          },
          "blocker": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "profile",
              "economics",
              "tracking",
              "live",
              null
            ],
            "description": "The first of the completion gates (`profile`, `economics`, `tracking`, `live`, in that order) whose status is not `pass`; null when all four pass. `payout` and `payments` never block."
          },
          "gates": {
            "type": "object",
            "properties": {
              "profile": {
                "type": "object",
                "properties": {
                  "status": {
                    "$ref": "#/components/schemas/GateStatus"
                  },
                  "next_action": {
                    "type": "string",
                    "description": "e.g. complete_program_profile"
                  }
                }
              },
              "economics": {
                "type": "object",
                "properties": {
                  "status": {
                    "$ref": "#/components/schemas/GateStatus"
                  },
                  "next_action": {
                    "type": "string",
                    "description": "e.g. set_commission_policy"
                  }
                }
              },
              "payout": {
                "type": "object",
                "description": "The advertiser's Stripe Connect. Reported for billing; never blocks completion.",
                "properties": {
                  "status": {
                    "$ref": "#/components/schemas/GateStatus"
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "stripe",
                      "manual"
                    ]
                  },
                  "next_action": {
                    "type": "string",
                    "description": "e.g. surface_stripe_connect_url"
                  },
                  "endpoint": {
                    "type": "string",
                    "description": "Stripe-connect URL endpoint when payout fails."
                  }
                }
              },
              "payments": {
                "type": "object",
                "description": "Passes when the program can record sales: its Stripe account is\nconnected, or your server has sent a sale to\n`/api/v1/track/sale` (a test sale counts; the synthetic chain's\nsale does not). Required to go live; never blocks completion.\n",
                "properties": {
                  "status": {
                    "$ref": "#/components/schemas/GateStatus"
                  },
                  "method": {
                    "type": "string",
                    "enum": [
                      "stripe_connect",
                      "s2s"
                    ],
                    "description": "The program's payment tracking mode. It picks `next_action`; either way of recording sales passes the gate."
                  },
                  "next_action": {
                    "type": "string",
                    "description": "connect_stripe or send_test_sale"
                  }
                }
              },
              "tracking": {
                "type": "object",
                "properties": {
                  "status": {
                    "$ref": "#/components/schemas/GateStatus"
                  },
                  "detail": {
                    "type": "string",
                    "description": "e.g. tracker_installed | receiving_events"
                  },
                  "checked_at": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date-time"
                  },
                  "next_action": {
                    "type": "string",
                    "description": "e.g. install_tracker_or_send_events"
                  }
                }
              },
              "live": {
                "type": "object",
                "properties": {
                  "status": {
                    "$ref": "#/components/schemas/GateStatus"
                  },
                  "test_chain": {
                    "$ref": "#/components/schemas/TestChainVerdict"
                  },
                  "next_action": {
                    "type": "string",
                    "description": "e.g. run_synthetic_sale_chain"
                  }
                }
              }
            }
          },
          "last_event_at": {
            "type": "object",
            "description": "Most recent REAL (non-test) event timestamp per type, or null.",
            "properties": {
              "click": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "lead": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "sale": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          },
          "counts_24h": {
            "type": "object",
            "properties": {
              "clicks": {
                "type": "integer"
              },
              "leads": {
                "type": "integer"
              },
              "sales": {
                "type": "integer"
              }
            }
          },
          "key_rotated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "poll": {
            "type": "object",
            "properties": {
              "retry_after_seconds": {
                "type": "integer",
                "description": "Seconds to wait between polls."
              }
            }
          }
        }
      }
    }
  }
}
