{
  "openapi": "3.0.3",
  "info": {
    "title": "CreatoRain Partner API",
    "version": "1.0.0",
    "description": "The surface an outside system (a POS or ordering platform) uses to enrol its merchants'\nstores with CreatoRain, and later to read those stores' reservations and published content.\n\n## Authentication\n\nOne long-lived API key per partner, sent as `Authorization: Bearer <key>`. Keys look like\n`crp_live_...` on production and `crp_test_...` everywhere else; the tag is a label for\nhumans and is not what grants access. A key may also carry an IP allowlist, in which case a\nrequest from any other address is refused with `403 forbidden_ip`.\n\nA key is scoped to ONE account and ONE campaign. It cannot read another customer's data, and\nit is not an administrative credential.\n\nTHE HEADER IS `Authorization`, on every route and for both credentials. There is no `x-api-key`\nheader on this API and a key sent in one is not read, so a request carrying only that answers\n`401 unauthorized`. Said plainly because it is the wrong guess an integrator makes once and\nthen spends an afternoon on: the refusal looks identical to a bad key.\n\n## Errors\n\nEvery failing response carries the same envelope, with a stable machine-readable `code`:\n\n```json\n{ \"error\": { \"code\": \"location_already_exists\", \"message\": \"...\", \"field\": \"partner_location_id\" } }\n```\n\nBranch on `code`, never on `message`. `field` is present when one request field is at fault.\n\n## Rate limit\n\n600 requests per minute per key by default, over a 60 second window. Over the ceiling the\nanswer is `429` with `code: rate_limited` and the standard `RateLimit-*` headers.\n\n## Time\n\nEvery time value is RFC 3339 with the STORE'S offset (`2026-09-11T15:00:00-07:00`), and the\nobject carries a sibling `timezone` field holding the IANA name (`America/Los_Angeles`).\nStores span time zones, so a UTC-only value renders \"3 pm today\" wrongly in a merchant UI.\n\n## Monthly pacing\n\nA store's plan is paced by the month. Months are calendar months on the STORE'S clock, and a\nseason is three of them, counted from the month that holds the season start (the store-local\nday of `approved_at`). In each month a store is offered to creators until the season's slots\nbooked or in flight reach `videos_per_month` for each month of the season so far (so a\nmonth that booked fewer than its share leaves the rest to the next month of the same season),\nnever more than the season total. Then the store is not offered again until the 1st of the\nnext month, store time. Nothing carries across a season boundary. A `plan` change by PATCH\napplies from the 1st of the next month. The whole-term total (`videos_per_month` x\n`term_months`) still applies on top.\n\n## Deprecations\n\n`quarter_target` is renamed `season_target` (videos per season: `videos_per_month` x 3) in\n`GET /locations/{id}` `progress` and in the `location.activated` webhook. On the\n`location.quarter_summary` webhook the target is sent as `season_target`, and the old `target`\nfield carries the same value. For ONE release both names are sent with the same value; read\n`season_target`. Before this release `quarter_target` and `target` held the whole-term total.\n"
  },
  "servers": [
    {
      "url": "https://data.creatorain.com/partner/v1",
      "description": "Production (live stores; a live key is issued per partner)"
    },
    {
      "url": "https://datadev.creatorain.com/partner/v1",
      "description": "Sandbox, for integration testing (test keys, test stores)"
    }
  ],
  "security": [
    {
      "partnerApiKey": []
    }
  ],
  "tags": [
    {
      "name": "Locations",
      "description": "Enrolling a merchant's store and keeping its state"
    },
    {
      "name": "Reads",
      "description": "What a store looks like now, its bookings and its published videos"
    },
    {
      "name": "Acknowledgement",
      "description": "Telling us a human at the restaurant saw a booking"
    },
    {
      "name": "Billing",
      "description": "The monthly reconciliation detail, per store"
    },
    {
      "name": "Pricing",
      "description": "Finished prices (you do no price maths) and your surcharge setting"
    }
  ],
  "paths": {
    "/locations": {
      "post": {
        "tags": [
          "Locations"
        ],
        "summary": "Enrol a store",
        "operationId": "createPartnerLocation",
        "description": "Called once when a merchant approves joining. `partner_location_id` is YOUR id for the\nstore and is the idempotency key, so a retry is safe:\n\n| State when the call arrives                       | Answer                              |\n|---------------------------------------------------|-------------------------------------|\n| No store with this `partner_location_id`          | `201` and the store is created      |\n| It exists with the same name and address          | `200` with the same body plus a `warning` |\n| It exists with a different name or address        | `409 location_already_exists`       |\n| It exists and its contract was terminated         | `201`, the store is re-activated    |\n\nA repeat of a store that is still live is never an error for an identical body: it answers\n`200` with the same body and a top-level `warning` object,\n`{\"code\": \"location_already_exists\", \"message\": \"This location already exists\"}`, so your\nsystem can tell a repeat from a first create. Nothing is written. A different name or\naddress for a live store is `409` with the same `code` and `message`.\n\nA store whose contract was terminated is added again directly with the same call: it is\nre-activated (the same store and history, not a second one), it gets a NEW price lock from\nthe plan you send, it is offered to creators again, and `location.activated` is sent again.\nThere is no warning. Its hours and timezone are kept (change them with PATCH).\n\nA changed `partner_location_id` is a DIFFERENT store, so the id must be stable in your\nsystem. Use `PATCH /locations/{partner_location_id}` to change a store's name, address or\nplan; a create with changed details is refused rather than silently applied.\n\n## Store facts worth sending\n\n- **`timezone`**: the store's IANA zone. When sent it wins over the zone we derive from the\n  address state, which is an hour off in states with two zones (El Paso, the Florida\n  panhandle). Every time we give you for the store, every reminder and the visit-day text\n  use it.\n- **`opening_hours`**: the week from your POS. When sent we use it and do not look the store\n  up; `hours.source` reads `partner`, and the store is never held for missing hours. When\n  not sent we look the hours up ourselves as before.\n- **`merchant_contact`**: who at the restaurant hears from us. Its email receives our store\n  notices instead of your account owner, and its phone receives the visit-day text.\n- **`approval_mode`**: `auto` (default) or `sample_review`.\n\nA repeat of a create (same id, name and address) is `200` and writes nothing, whatever\nextra fields it carries. To change any of these on an existing store, use `PATCH`.\n\n## What a new store starts with, without you sending it\n\n- **Creator bookings are AUTO-CONFIRMED.** A creator who books a slot at your store has a\n  confirmed visit immediately; there is no approval step waiting on somebody at the\n  restaurant, and per-booking review is a later product. Your merchant hears about the\n  booking through the notice we send and, if you have wired it, through the\n  `reservation.confirmed` webhook.\n- **A creator visit window inside the store's opening hours**, with our house rules on top:\n  no Saturday visits, Friday visits end at 17:00, Sunday visits start at 15:00. A break in\n  the day (an afternoon closure) is kept, so no creator is offered a time when the door is\n  shut. An operator can change the window per store afterwards.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateLocationRequest"
              },
              "examples": {
                "example": {
                  "summary": "A store joining",
                  "value": {
                    "partner_location_id": "store_123",
                    "name": "Example Noodle House",
                    "address": {
                      "line1": "100 Main St",
                      "city": "San Jose",
                      "state": "CA",
                      "postal_code": "95113",
                      "country": "US"
                    },
                    "plan": {
                      "videos_per_month": 2,
                      "term_months": 3
                    },
                    "signature_dishes": [
                      {
                        "name": "Signature tempura platter",
                        "image_url": "https://cdn.example.com/tempura.jpg"
                      }
                    ],
                    "preferred_language": "zh",
                    "approved_at": "2026-09-09T18:22:00Z",
                    "approved_by": "ops@example.com",
                    "approval_mode": "auto",
                    "timezone": "America/Los_Angeles",
                    "opening_hours": {
                      "mon": [
                        [
                          "11:00",
                          "15:00"
                        ],
                        [
                          "17:00",
                          "21:30"
                        ]
                      ],
                      "tue": [
                        [
                          "11:00",
                          "21:30"
                        ]
                      ],
                      "wed": [
                        [
                          "11:00",
                          "21:30"
                        ]
                      ],
                      "thu": [
                        [
                          "11:00",
                          "21:30"
                        ]
                      ],
                      "fri": [
                        [
                          "11:00",
                          "22:00"
                        ]
                      ],
                      "sat": [
                        [
                          "11:00",
                          "22:00"
                        ]
                      ],
                      "sun": [
                        [
                          "11:00",
                          "21:00"
                        ]
                      ]
                    },
                    "merchant_contact": {
                      "name": "Alex Kim",
                      "phone": "+14155550100",
                      "email": "owner@example.com"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The store already existed with the same name and address. The body is the same as the\none the original create returned, plus a top-level `warning`\n(`location_already_exists`, \"This location already exists\"). Nothing was written.\n",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Location"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "warning": {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "location_already_exists"
                              ]
                            },
                            "message": {
                              "type": "string",
                              "example": "This location already exists"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "201": {
            "description": "The store was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Location"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`. `field` names the offending path, for example `plan.term_months`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized`. Missing, malformed, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`forbidden_ip`. A valid key presented from an address outside its allowlist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`location_already_exists` when this `partner_location_id` already names a live store with a\ndifferent name or address.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "already_exists": {
                    "value": {
                      "error": {
                        "code": "location_already_exists",
                        "message": "This location already exists",
                        "field": "partner_location_id"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`. Over this key's requests-per-minute ceiling.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`internal_error`. Never carries a cause; quote the reference in the message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/locations/{partner_location_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PartnerLocationId"
        }
      ],
      "get": {
        "tags": [
          "Reads"
        ],
        "summary": "One store's current state, progress and next visit",
        "operationId": "getPartnerLocation",
        "description": "The store as we hold it, plus this quarter's progress and the next confirmed creator visit.\nThis is the read the embedded merchant dashboard is built from.\n\nEITHER CREDENTIAL: your partner key (any store of the key), or that store's own 30 minute\ntoken from `POST /locations/{partner_location_id}/dashboard-token`, sent as\n`Authorization: Bearer <token>` or `?token=<token>`. A store token presented for any other\nstore answers `401 unauthorized`, and it answers the same 401 whether or not that other\nstore exists.\n\n`published_count` counts videos filmed at THIS store that are live. Videos published before\n2026-09-09 carry no store and are not counted; that is a property of the data, not of the\nquery.\n\n`rules` is this store's matching terms as they now stand, in the same shape `PATCH` accepts.\nThis is the read to reconcile against if a `PATCH` response was ever lost.\n",
        "security": [
          {
            "partnerApiKey": []
          },
          {
            "storeTokenHeader": []
          },
          {
            "storeTokenQuery": []
          }
        ],
        "responses": {
          "200": {
            "description": "The store, its progress and its next visit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LocationState"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/LocationNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "tags": [
          "Locations"
        ],
        "summary": "Change a store's name, address, plan, dishes, language, hours, time zone or contact",
        "operationId": "patchPartnerLocation",
        "description": "Every field is optional and AT LEAST ONE is required. Send only what changed.\n\nA changed `plan` recomputes the store's total deliverables (`videos_per_month` x\n`term_months`) and its collaboration end date. The START date is never rewritten: the store\nbegan being offered when it began. A PARTIAL plan is merged with the stored one, so\n`{\"plan\": {\"videos_per_month\": 4}}` on a 2 x 3 store means 4 x 3. A new `videos_per_month`\napplies to the monthly pacing from the 1st of the NEXT month, store time (see Monthly\npacing above); the month under way keeps the plan it started with.\n\nAn `address` is replaced WHOLE rather than merged field by field, so send the complete\naddress object. The store's time zone is re-derived from it, unless you have sent a\n`timezone`, which keeps winning until you send `\"timezone\": null`.\n\n`opening_hours` REPLACES the store's week, whatever found the old one, and from then on no\nlookup of ours overwrites it. `merchant_contact` changes only the keys you send; a key sent\nas `null` clears it (a cleared `email` sends our notices back to your account owner).\n\n`rules` sets WHICH CREATORS this store will take: an array of\n`{attribute, operator, value}`. It REPLACES the store's rules for the attributes you send\nand leaves the others alone, so sending `platform` rules does not touch a follower band.\nUse `clear_rules` with a list of attribute names to remove terms outright.\n\nTHE RESPONSE CARRIES THE STORE'S RULES AS THEY NOW STAND, read back from storage rather\nthan echoed from your request, so one call both writes and confirms. Replace-by-attribute\nmeans the array you get back is usually LONGER than the one you sent: send one `platform`\nrule to a store that also has a follower band and the response carries both.\n\nOnly `follower_count`, `platform` and `language` may be set. `ethnicity`, `age_band` and\n`gender` are refused with `422 validation_error`: filtering paid work by a protected\ncharacteristic needs a written policy that does not exist, and we will not store the rule\nuntil it does.\n\nThere is no `plan_tier` field. We store no tier per store, and an unknown field is refused\nby name rather than accepted and dropped, so a value can never be silently lost.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PatchLocationRequest"
              },
              "examples": {
                "rename": {
                  "summary": "A store changed its name",
                  "value": {
                    "name": "Example Noodle House Downtown"
                  }
                },
                "upgrade": {
                  "summary": "The merchant bought more videos",
                  "value": {
                    "plan": {
                      "videos_per_month": 4
                    }
                  }
                },
                "matching": {
                  "summary": "This store only wants creators with 20k+ on TikTok",
                  "value": {
                    "rules": [
                      {
                        "attribute": "follower_count",
                        "operator": "gte",
                        "value": {
                          "count": 20000,
                          "platform": "tiktok"
                        }
                      },
                      {
                        "attribute": "platform",
                        "operator": "eq",
                        "value": "tiktok"
                      }
                    ]
                  }
                },
                "matching_cleared": {
                  "summary": "Drop the follower bar, keep everything else",
                  "value": {
                    "clear_rules": [
                      "follower_count"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The store after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Location"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/LocationNotFound"
          },
          "409": {
            "description": "`location_terminated`. A store whose contract has ended cannot be changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/locations/{partner_location_id}/pause": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PartnerLocationId"
        }
      ],
      "post": {
        "tags": [
          "Locations"
        ],
        "summary": "Pause scheduling at a store",
        "operationId": "pausePartnerLocation",
        "description": "Stops us offering this store to creators from the moment of the call.\n\nCONFIRMED BOOKINGS ARE NOT CANCELLED. One cancellation usually loses that creator\npermanently, and creator supply is shared across a city. If the store cannot host a visit\nthat is already booked, ask us to reschedule it.\n\nIDEMPOTENT: a store that is already paused answers `200` with its current state, writes\nnothing, and keeps the instant of the FIRST pause, which is what a merchant's \"paused\nsince\" line is made of.\n\nWEBHOOK: a pause that changes the store sends one `location.paused` event (`data`:\n`location_id`, `partner_location_id`, `reason`, `effective_at`, `timezone`,\n`initiated_by`). A repeat sends nothing. A pause or resume made by our own team sends the\nsame event with `initiated_by: creatorain`. `resume` sends `location.resumed` and\n`terminate` sends `location.terminated` the same way (once per change).\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PauseRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The store, now `paused`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Location"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/LocationNotFound"
          },
          "409": {
            "description": "`location_terminated`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/locations/{partner_location_id}/resume": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PartnerLocationId"
        }
      ],
      "post": {
        "tags": [
          "Locations"
        ],
        "summary": "Resume scheduling at a store",
        "operationId": "resumePartnerLocation",
        "description": "Offers the store to creators again and clears the pause reason.\n\nIDEMPOTENT: a store that is already active answers `200` and writes nothing. A TERMINATED\nstore is never resumed; it answers `409 location_terminated`.\n",
        "responses": {
          "200": {
            "description": "The store, now `active`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Location"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/LocationNotFound"
          },
          "409": {
            "description": "`location_terminated`. A terminated contract cannot be resumed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/locations/{partner_location_id}/terminate": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PartnerLocationId"
        }
      ],
      "post": {
        "tags": [
          "Locations"
        ],
        "summary": "End a store's contract",
        "operationId": "terminatePartnerLocation",
        "description": "Records that the contract has ended, stops the store being offered, and keeps every row.\nNothing is deleted: the visits completed and videos published at the store are what its\nfinal invoice is made of.\n\n`effective_at` is when the CONTRACT ends and may be in the future (notice given in advance)\nor in the past (it ended before anybody told us). It is a different fact from when you told\nus, which we record separately.\n\nFINAL. A second call answers `200` with the same body and writes nothing, so a retry cannot\nrewrite the record. There is no un-terminate over the API; ask us if a contract has to be\nreinstated.\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TerminateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The store, now `terminated`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Location"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/LocationNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/locations/{partner_location_id}/dashboard-token": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PartnerLocationId"
        }
      ],
      "post": {
        "tags": [
          "Reads"
        ],
        "summary": "Mint a 30 minute read-only token for one store",
        "operationId": "createPartnerLocationDashboardToken",
        "description": "Issues a credential whose entire authority is ONE store, for 30 minutes, read only. This is\nwhat makes the embedded merchant dashboard possible: the merchant's browser holds this\ntoken and never your master key.\n\nMint one when you render the embed. It cannot be refreshed and it cannot mint another one:\nthis route requires the PARTNER KEY, so a leaked merchant URL expires rather than renewing.\n\nThe token is accepted by the three `GET /locations/{partner_location_id}...` routes and\nnowhere else, as `Authorization: Bearer <token>` or `?token=<token>`.\n\n`dashboard_url` is omitted when this deployment hosts no embedded dashboard.\n",
        "responses": {
          "200": {
            "description": "A fresh token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DashboardToken"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/LocationNotFound"
          },
          "409": {
            "description": "`location_terminated`. No token is minted for a store with no contract.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`internal_error`. The store token feature is not enabled on this deployment. A\nconfiguration state, not a fault: tell us and we will switch it on.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/locations/{partner_location_id}/reservations": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PartnerLocationId"
        },
        {
          "$ref": "#/components/parameters/From"
        },
        {
          "$ref": "#/components/parameters/To"
        },
        {
          "$ref": "#/components/parameters/ReservationStatus"
        },
        {
          "$ref": "#/components/parameters/Acknowledged"
        },
        {
          "$ref": "#/components/parameters/Cursor"
        },
        {
          "$ref": "#/components/parameters/Limit"
        }
      ],
      "get": {
        "tags": [
          "Reads"
        ],
        "summary": "One store's creator bookings",
        "operationId": "getPartnerLocationReservations",
        "description": "The main query behind a merchant's dashboard. Newest touched first.\n\nEITHER CREDENTIAL: the partner key or that store's own token.\n\n`acknowledged=false` narrows it to the bookings nobody at the restaurant has confirmed\nseeing, which is what a merchant's own \"still to confirm\" panel filters on.\n\nNO PERSONAL DATA IS RETURNED, and there is no free-text search parameter, because our\nreservation search index is built from exactly the fields this projection withholds.\n",
        "security": [
          {
            "partnerApiKey": []
          },
          {
            "storeTokenHeader": []
          },
          {
            "storeTokenQuery": []
          }
        ],
        "responses": {
          "200": {
            "description": "A page of bookings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReservationPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/LocationNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/locations/{partner_location_id}/content": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PartnerLocationId"
        },
        {
          "$ref": "#/components/parameters/Cursor"
        },
        {
          "$ref": "#/components/parameters/Limit"
        }
      ],
      "get": {
        "tags": [
          "Reads"
        ],
        "summary": "One store's published videos",
        "operationId": "getPartnerLocationContent",
        "description": "Videos filmed at this store that are LIVE on a platform, newest first. Approved but\nunpublished work is not listed: a merchant's dashboard counts what is on the internet.\n\n`metrics` is `null` until the video's numbers have first been read from the public post,\nwhich is deliberate: `{\"views\": 0}` would be a claim that a video got no views, and `null`\nsays nobody counted. Once it has been read, `metrics` is an object and any single number\nthe platform does not publish is `null`, never 0. `metrics_measured_at` gives the time\neach number was read, and is null beside a number that was never read.\n\nEITHER CREDENTIAL: the partner key or that store's own token.\n",
        "security": [
          {
            "partnerApiKey": []
          },
          {
            "storeTokenHeader": []
          },
          {
            "storeTokenQuery": []
          }
        ],
        "responses": {
          "200": {
            "description": "A page of published videos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContentPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/LocationNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/locations/{partner_location_id}/creators": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PartnerLocationId"
        },
        {
          "$ref": "#/components/parameters/Cursor"
        },
        {
          "$ref": "#/components/parameters/Limit"
        }
      ],
      "get": {
        "tags": [
          "Reads"
        ],
        "summary": "One store's named creators (accepted or later)",
        "operationId": "getPartnerLocationCreators",
        "description": "The creators of this store's current season who are at stage `accepted` or later, newest\naccepted first. Creators still being contacted are not named; they are counts in\n`pipeline` on `GET /locations/{partner_location_id}`, and `pipeline.accepted` is the\nlength of this list.\n\nNo email, phone, rates or notes: a merchant should never need to contact a creator\ndirectly.\n\nEITHER CREDENTIAL: the partner key or that store's own token.\n",
        "security": [
          {
            "partnerApiKey": []
          },
          {
            "storeTokenHeader": []
          },
          {
            "storeTokenQuery": []
          }
        ],
        "responses": {
          "200": {
            "description": "A page of named creators.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StoreCreatorPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/LocationNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/locations/{partner_location_id}/creator-samples": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PartnerLocationId"
        }
      ],
      "get": {
        "tags": [
          "Reads"
        ],
        "summary": "The current batch of sample creators (sample review stores)",
        "operationId": "getPartnerCreatorSamples",
        "description": "For a store with `approval_mode: sample_review`: 5 sample creators that show the range of\ncreators the store will get, and a summary (follower range, common topics, audience).\nSamples are not the final list. We send `location.samples_ready` when a batch is ready.\n\n`status` is `preparing` (a batch is being prepared, `creators` is empty), `ready` (waiting\nfor your decision) or `approved` (recruiting follows; `location.recruiting` when it\nstarts).\n\nPublic profile only: no email, phone, name or rates.\n\nEITHER CREDENTIAL: the partner key or that store's own token.\n",
        "security": [
          {
            "partnerApiKey": []
          },
          {
            "storeTokenHeader": []
          },
          {
            "storeTokenQuery": []
          }
        ],
        "responses": {
          "200": {
            "description": "The current batch.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatorSamples"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/LocationNotFound"
          },
          "409": {
            "description": "`conflict`. The store does not use sample review.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/locations/{partner_location_id}/creator-samples/{batch_id}/decision": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PartnerLocationId"
        },
        {
          "name": "batch_id",
          "in": "path",
          "required": true,
          "description": "The `batch_id` from the samples read or the `location.samples_ready` event.",
          "schema": {
            "type": "string",
            "example": "smp_1b4e28ba-2fa1-4d3b-a3f5-ef19b5a7633b"
          }
        }
      ],
      "post": {
        "tags": [
          "Locations"
        ],
        "summary": "Approve the samples, or ask for a new batch",
        "operationId": "decidePartnerCreatorSamples",
        "description": "`approve`: recruiting starts, and `location.recruiting` follows when it does.\n`new_batch`: we prepare a new set that never repeats earlier samples, and send\n`location.samples_ready` again when it is ready. After two new batches our CSM calls the\nmerchant.\n\nThe same decision again answers 200 with the same body. The other decision on a batch\nalready decided is 409, as is a decision on a batch that was replaced.\n\nPARTNER KEY ONLY.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SampleDecisionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The decision as recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SampleDecision"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "`location_not_found`, or `sample_batch_not_found` when no batch of this store has that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`conflict` (already decided the other way, replaced, or not a sample review store) or `location_terminated`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/reservations": {
      "parameters": [
        {
          "$ref": "#/components/parameters/From"
        },
        {
          "$ref": "#/components/parameters/To"
        },
        {
          "$ref": "#/components/parameters/ReservationStatus"
        },
        {
          "$ref": "#/components/parameters/Acknowledged"
        },
        {
          "$ref": "#/components/parameters/Cursor"
        },
        {
          "$ref": "#/components/parameters/Limit"
        }
      ],
      "get": {
        "tags": [
          "Reads"
        ],
        "summary": "Every store's bookings, for your operations desk",
        "operationId": "getPartnerReservations",
        "description": "The same rows as the per-store list, across every store enrolled under your key, each row\nalso carrying `partner_location_id` and `location_id` so it can be attributed.\n\nYOUR \"NEEDS FOLLOW-UP\" LIST IS THIS CALL WITH FOUR PARAMETERS:\n\n```\nGET /partner/v1/reservations?status=confirmed&from=2026-09-11&to=2026-09-12&acknowledged=false\n```\n\nThose are the visits arriving that nobody at the restaurant has confirmed seeing, and they\nare the same bookings `unacknowledged_upcoming` on `GET /summary` counts, so a desk can go\nfrom the number straight to the rows.\n\nPARTNER KEY ONLY. A store token cannot read this: a merchant's browser must never be able\nto read the whole chain's schedule, which is the reason the per-store credential exists.\n",
        "responses": {
          "200": {
            "description": "A page of bookings across every store of this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReservationPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/reservations/{reservation_id}/acknowledge": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ReservationId"
        }
      ],
      "post": {
        "tags": [
          "Acknowledgement"
        ],
        "summary": "The merchant saw this booking",
        "operationId": "acknowledgePartnerReservation",
        "description": "Called when the merchant presses confirm in YOUR UI. Tell us which action it was:\n`forwarded_to_manager` (they passed it to whoever is on the floor) or `acknowledged_self`\n(they dealt with it themselves).\n\nThis is the one thing in the whole chain we cannot see. We know the notice went out, we\nknow the creator confirmed, and we find out afterwards whether anybody arrived. Whether a\nperson at the restaurant actually read it is yours to tell us, and it is the link that\nturns out to have been broken when a visit goes wrong.\n\nIDEMPOTENT PER `(reservation_id, kind)`. A retry answers `200` with the instant we ALREADY\nHOLD, never a new one: that instant is when the merchant pressed the button, and a lost\nresponse must not be able to move it. The two kinds are independent, so a booking that was\nforwarded AND acknowledged carries both, each with its own time.\n\nPARTNER KEY ONLY. A per-store dashboard token cannot write here, which is the reason it\nstays a browser-safe credential: send the acknowledgement from your own backend.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AcknowledgeRequest"
              },
              "examples": {
                "forwarded": {
                  "summary": "Passed to the manager on duty",
                  "value": {
                    "kind": "forwarded_to_manager"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recorded. The same body on the first call and on every repeat.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Acknowledgement"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/ReservationNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/summary": {
      "get": {
        "tags": [
          "Reads"
        ],
        "summary": "Your whole estate, in one call",
        "operationId": "getPartnerSummary",
        "description": "The morning screen for your operations desk: how many stores are running, how many are\npaused, what has been published this month, what is arriving over the next fortnight, how\nmany bookings a human at a restaurant confirmed seeing, and how many visits arrive within\n24 hours that NOBODY has acknowledged.\n\nThat last number is what this endpoint exists for. Every one of those bookings is a creator\nwho may walk into a restaurant that is not expecting them, and the list behind the number is\n`GET /reservations?status=confirmed&from=<today>&to=<tomorrow>&acknowledged=false`.\n\nNO PARAMETERS, deliberately. Every window here is fixed (this month, the next 14 days, the\nnext 24 hours) so that today's screen can be compared with yesterday's. When you need a\nwindow of your own, the lists take one.\n\n## Which clock each number is on\n\nYour stores span time zones, so there is no single clock this body could be stated on and it\ndoes not pretend otherwise. `published_this_month` and\n`reminders_acknowledged_this_month` are bucketed in EACH STORE'S OWN local month, exactly as\n`GET /billing/monthly` buckets them, so a tile and an invoice can never disagree.\n`visits_next_14_days` and `unacknowledged_upcoming` are measured on each store's own wall\nclock, which matters most for the 24 hour window: measured in UTC, a third of a US west\ncoast day would fall in or out of it.\n\n`generated_at` is the only TIME on this body. It is an instant about our run rather than a\ntime at any restaurant, and `timezone` is its zone, which is always `UTC`.\n\nPARTNER KEY ONLY. A store token cannot read this: the state of the whole chain is your\nbusiness, not one merchant's.\n",
        "responses": {
          "200": {
            "description": "The estate as it stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Summary"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/billing/monthly": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Month"
        },
        {
          "$ref": "#/components/parameters/Cursor"
        },
        {
          "$ref": "#/components/parameters/Limit"
        }
      ],
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "That month's reconciliation detail, per store",
        "operationId": "getPartnerMonthlyBilling",
        "description": "One row per store enrolled under your key, except that a TERMINATED store with nothing to\nreconcile in the month (no video published, no visit completed and no no-show) is left out\ninstead of listed as a line of zeros. A terminated store that did have any of those stays,\nwith `status: terminated`. A page can therefore hold fewer rows than `limit`: follow\n`next_cursor` until it is null. Each row: videos published in the month, the plan the\nstore is on, completed visits and no-shows, and the finished amounts for the month. This\nis what an invoice is checked against. You do no price maths: `amount_due` is what we bill\nyou for the store, `merchant_amount` is what the merchant owes with your surcharge included.\n\nTHE AMOUNTS. `billed_videos` = `videos_published` - `makeup_videos` (free replacement videos\nare never billed). `merchant_amount` = `billed_videos` x (the flat merchant price + your\nsurcharge, both locked on the store for THAT month). `amount_due` is what we bill you: for\neach billed video, that video's actual creator cost plus our fee per video (`fee_per_video`\nin the pricing block), summed. A video whose creator was paid nothing (comped) still carries\nthe fee. It is not a fixed price per video. A plan or approval change re-locks from the next\nmonth, so each month uses its own lock. A store with no locked price (confirmed before\npricing) is billed the same way from the current default terms: creator cost plus our fee,\nand `merchant_amount` from the default merchant price. Both are null only when pricing is\nnot set up at all. While a billed\nvideo's creator cost is not known yet, `amount_due` is null and `cost_pending_videos` says\nhow many videos are waiting; it is never a 0 that under-bills. See `amounts_note`. Currency\nunits, like every price on this API.\n\nTHE MONTH IS THE STORE'S MONTH. A video published at 23:30 on 30 September in Los Angeles\nis a September video; bucketed in UTC it would be 06:30 on 1 October and would land on the\nwrong invoice, with both sides looking at a real timestamp and reading it in different\nclocks. Each store's window is resolved on that store's own clock, and it is half open:\n`[local 1st 00:00, local 1st of next month 00:00)`, so no video is counted twice or lost.\n\nVisits are counted by VISIT DATE, which our reservations already store in the venue's own\ncalendar, so a visit marked completed in October for a September booking is a September\nvisit.\n\n`month` is REQUIRED and has no default. \"This month\" would be the obvious default and is\nthe wrong one here: a scheduled job whose parameter went missing would silently start\nreporting whichever month it ran in, and the first sign of that is a merchant charged\ntwice.\n\n`note` states the one limitation of these numbers and is on every response. Our per-store\npin on content landed on 2026-09-09, so anything published before that date carries no\nstore and is not counted here.\n\nPARTNER KEY ONLY. A store token cannot read this: a chain's billing is your business, not\none merchant's.\n",
        "responses": {
          "200": {
            "description": "A page of stores, with the month's counts for each.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BillingMonth"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/pricing/quote": {
      "get": {
        "tags": [
          "Pricing"
        ],
        "summary": "The finished price for a merchant's choices, before a store exists",
        "operationId": "getPartnerPricingQuote",
        "description": "Ask for a quote on the apply screen and the Confirm screen. You do NO price maths: every\nnumber is ready to show. Show `merchant.*` on the merchant's screens, and `creatorain` and\n`surcharge` on your sales screen. Change a choice and ask again; the answer reflects it.\n\n- `merchant.per_video` is the price per published video you charge the merchant, with your\n  surcharge and any plan surcharge already included (base price + `surcharge.per_video` +\n  `plan_surcharge.per_video`). It is the same for every city. `per_month` = per video x videos per\n  month, `per_season` = per month x 3. `season_videos` = videos per month x 3.\n- `creatorain` is what we bill you, per published video: the creator's actual cost plus our\n  fee. `fee_per_video` is our fee. The creator cost varies per video, so it is not a fixed\n  price: `creator_cost_per_video` is an estimate range (or null), the bill uses the actual cost.\n- `surcharge.per_video` is your surcharge (`PUT /settings`; a default applies until you set\n  one, so editing it changes what merchants see). It is shown so you can see your share, and\n  is already included in `merchant.per_video` (do not add it again).\n- `plan_surcharge` is its own line for a non-standard plan: fewer than 2 videos a month, or a\n  term below 3 months (send `term_months` to include it). Such a plan is accepted, and\n  `plan_surcharge.per_video` is added to the merchant price so you can show why it costs more\n  (`applies` is true; 0 and false for a standard plan). `placeholder: true` means the amount\n  is provisional and may change.\n- `valid_until` is the last day (UTC) the quote holds. A store confirmed by then is locked at\n  the price in force when it is confirmed.\n\nWe bill by published video. `postal_code` is optional; when sent it is checked as a US ZIP\n(a bad one is rejected), but there is no per-area pricing, so it does not change the price.\n\nPARTNER KEY ONLY.\n",
        "parameters": [
          {
            "name": "videos_per_month",
            "in": "query",
            "required": true,
            "description": "1 to 10. The standard plan is 2 to 10; 1 is accepted and adds the plan surcharge.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10
            },
            "example": 2
          },
          {
            "name": "term_months",
            "in": "query",
            "required": false,
            "description": "Contract length in months, 1 to 120. The standard terms are 3, 8 and 12 (longer is allowed); below 3 adds the plan surcharge.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 120
            },
            "example": 3
          },
          {
            "name": "approval_mode",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "auto",
                "sample_review"
              ],
              "default": "auto"
            }
          },
          {
            "name": "postal_code",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^\\d{5}(-\\d{4})?$"
            },
            "example": "95050"
          }
        ],
        "responses": {
          "200": {
            "description": "The quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PriceQuote"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/settings": {
      "get": {
        "tags": [
          "Pricing"
        ],
        "summary": "Your settings",
        "operationId": "getPartnerSettings",
        "description": "Your surcharge per video (a default applies until you set one; `updated_at` is null then), its currency, and when it was last set. PARTNER KEY ONLY.",
        "responses": {
          "200": {
            "description": "Your settings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerSettings"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "put": {
        "tags": [
          "Pricing"
        ],
        "summary": "Set your surcharge per video, once for all stores",
        "operationId": "putPartnerSettings",
        "description": "A JSON number from 0 to 10000, in whole cents (`10` or `10.5`, not `\"10\"`). It replaces the\ndefault surcharge. It applies to new quotes and to stores confirmed after it. Stores already confirmed keep the price they\nwere confirmed with until their plan changes. PARTNER KEY ONLY.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "surcharge_per_video"
                ],
                "additionalProperties": false,
                "properties": {
                  "surcharge_per_video": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000,
                    "example": 10
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The stored settings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerSettings"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "partnerApiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "The partner API key, issued out of band. `Authorization: Bearer crp_live_...`.\nNot a JWT: it is an opaque credential and carries no claims.\n"
      },
      "storeTokenHeader": {
        "type": "http",
        "scheme": "bearer",
        "description": "A PER-STORE, 30 minute, READ-ONLY token from\n`POST /locations/{partner_location_id}/dashboard-token`, sent as\n`Authorization: Bearer <token>`.\n\nAccepted by the three `GET /locations/{partner_location_id}...` routes and NOWHERE ELSE.\nPresented anywhere else, or for any other store, it answers `401 unauthorized`. This is\nwhat lets a merchant's browser read their own store without ever holding your master key.\n"
      },
      "storeTokenQuery": {
        "type": "apiKey",
        "in": "query",
        "name": "token",
        "description": "The same per-store token in the query string, because an embedded iframe cannot set a\nheader. When both are present the query string wins, since that is the URL the page was\nloaded with.\n"
      }
    },
    "parameters": {
      "PartnerLocationId": {
        "name": "partner_location_id",
        "in": "path",
        "required": true,
        "description": "YOUR id for the store, as you sent it to `POST /locations`.",
        "schema": {
          "type": "string",
          "maxLength": 200
        },
        "example": "store_123"
      },
      "ReservationId": {
        "name": "reservation_id",
        "in": "path",
        "required": true,
        "description": "OUR id for the booking, as it reached you in a `reservation.*` webhook or in a\nreservations list. A uuid: anything else is refused with `400 validation_error` naming\n`reservation_id`, which tells you to look at your code rather than at your data.\n",
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "example": "7f1c2b64-6f6a-4a7d-9a0e-1c2b647f1c2b"
      },
      "Month": {
        "name": "month",
        "in": "query",
        "required": true,
        "description": "The month to report, `YYYY-MM`, in each STORE'S own calendar. Required, with no default.\n",
        "schema": {
          "type": "string",
          "pattern": "^\\d{4}-(0[1-9]|1[0-2])$"
        },
        "example": "2026-09"
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "The `next_cursor` from the previous page, sent back UNCHANGED. Omit it for the first page.\n\nIt is opaque: do not build one yourself. It names a POSITION in the sort order rather than\na count of rows to skip, which is why a page stays stable when a booking is touched between\ntwo requests. A cursor we did not issue is refused with `400 validation_error`, rather than\nsilently treated as the first page, so a broken client fails loudly instead of looping over\npage one for ever.\n",
        "schema": {
          "type": "string"
        }
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Rows per page. Default 50, maximum 200. A larger value is CLAMPED to 200 rather than\nrefused, so you never have to guess the ceiling.\n",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 200,
          "default": 50
        }
      },
      "From": {
        "name": "from",
        "in": "query",
        "required": false,
        "description": "Earliest VISIT DATE to include, `YYYY-MM-DD` in the store's own calendar. A value we cannot\nread is refused rather than ignored: a reconciliation run whose window was silently dropped\nwould read the whole book and report it as one month.\n",
        "schema": {
          "type": "string",
          "format": "date"
        },
        "example": "2026-09-01"
      },
      "To": {
        "name": "to",
        "in": "query",
        "required": false,
        "description": "Latest VISIT DATE to include, `YYYY-MM-DD`, inclusive.",
        "schema": {
          "type": "string",
          "format": "date"
        },
        "example": "2026-09-30"
      },
      "ReservationStatus": {
        "name": "status",
        "in": "query",
        "required": false,
        "description": "Return only bookings in this state.",
        "schema": {
          "type": "string",
          "enum": [
            "pending",
            "confirmed",
            "rejected",
            "cancelled",
            "completed",
            "no_show"
          ]
        }
      },
      "Acknowledged": {
        "name": "acknowledged",
        "in": "query",
        "required": false,
        "description": "`true` for bookings a human at the restaurant has confirmed seeing (you told us so with\n`POST /reservations/{reservation_id}/acknowledge`), `false` for the ones nobody has. Omit\nit for both.\n\n`1` and `0` are accepted as well. ANY OTHER VALUE IS REFUSED with `400 validation_error`\nrather than ignored, and that is on purpose: a follow-up list that silently returned every\nbooking would show your desk a queue of work that has already been done.\n",
        "schema": {
          "type": "boolean"
        },
        "example": false
      }
    },
    "responses": {
      "ValidationError": {
        "description": "`validation_error`. `field` names the offending parameter.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "`unauthorized`. A missing, malformed, unknown or revoked key; or an invalid, expired or\nwrong-store dashboard token. One answer for every case on purpose: the holder of a bad\ncredential learns nothing from the difference.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "LocationNotFound": {
        "description": "`location_not_found`. No store of this key carries that `partner_location_id`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ReservationNotFound": {
        "description": "`reservation_not_found`. No store enrolled under this key holds that booking. One answer\nwhether the booking belongs to another partner, to another account, or to nobody: a caller\nwho could tell those apart could count our bookings by walking ids.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "`rate_limited`. Over this key's requests-per-minute ceiling.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "WebhookEnvelope": {
        "type": "object",
        "required": [
          "event_id",
          "event",
          "occurred_at",
          "data"
        ],
        "description": "The JSON body of every webhook we send you. `level` is optional and only ever an extra\nfield: 1 = the store needs to know now (location paused, resumed or terminated; a booking\ncancelled or rescheduled; a creator about to arrive; samples ready), 2 = worth a look\n(creator accepted, recruiting started, content published, and the other lifecycle events),\n3 = background (metrics updates, acknowledgements). A receiver that ignores it keeps working.\n\n`content.metrics_updated` sends the video's five numbers (`views`, `likes`, `comments`,\n`shares`, `saves`; null when never read, never 0) and beside them `metrics_measured_at`\n(schema `MetricsMeasuredAt`): when each was read, null beside a number never read.\n",
        "properties": {
          "event_id": {
            "type": "string",
            "example": "01J0EXAMPLE000000000000000"
          },
          "event": {
            "type": "string",
            "example": "location.paused"
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-01T18:25:11Z"
          },
          "partner_location_id": {
            "type": "string",
            "nullable": true,
            "example": "store_001"
          },
          "location_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "level": {
            "type": "integer",
            "enum": [
              1,
              2,
              3
            ],
            "description": "Message level for this event, 1 (most important) to 3. See the schema description.",
            "example": 1
          },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "example": {
              "reason": "Closed for renovation",
              "initiated_by": "partner"
            }
          }
        }
      },
      "CreateLocationRequest": {
        "type": "object",
        "required": [
          "partner_location_id",
          "name"
        ],
        "additionalProperties": false,
        "description": "Deliberately small. Social accounts, cuisine, blackout dates and daily visit caps are NOT\naccepted: every extra field slows onboarding and is one more thing to get wrong, and we fill\nthose ourselves. `timezone`, `opening_hours` and `merchant_contact` are optional but worth\nsending: they remove the three most common reasons a store waits or a notice is missed. An\nunrecognised field is refused rather than ignored, so a value can never be silently\ndropped.\n",
        "properties": {
          "partner_location_id": {
            "type": "string",
            "maxLength": 200,
            "description": "Your id for the store. The idempotency key, and the store's identity. Must be stable.",
            "example": "store_123"
          },
          "name": {
            "type": "string",
            "maxLength": 300,
            "example": "Example Noodle House"
          },
          "address": {
            "$ref": "#/components/schemas/Address"
          },
          "plan": {
            "$ref": "#/components/schemas/Plan"
          },
          "signature_dishes": {
            "type": "array",
            "maxItems": 50,
            "items": {
              "type": "object",
              "required": [
                "name"
              ],
              "additionalProperties": false,
              "properties": {
                "name": {
                  "type": "string",
                  "maxLength": 300
                },
                "image_url": {
                  "type": "string",
                  "format": "uri",
                  "maxLength": 2000,
                  "nullable": true,
                  "description": "An http(s) link to the dish photo. Stored with the dish."
                }
              }
            }
          },
          "preferred_language": {
            "type": "string",
            "enum": [
              "zh",
              "en"
            ],
            "description": "The language the MERCHANT is addressed in. Omit when you do not know; it is not defaulted to en."
          },
          "approval_mode": {
            "$ref": "#/components/schemas/ApprovalMode"
          },
          "timezone": {
            "$ref": "#/components/schemas/StoreTimezone"
          },
          "opening_hours": {
            "$ref": "#/components/schemas/OpeningHours"
          },
          "merchant_contact": {
            "$ref": "#/components/schemas/MerchantContact"
          },
          "approved_at": {
            "type": "string",
            "format": "date-time",
            "description": "When your back office approved this store, RFC 3339. The store's collaboration start\ndate is the STORE-LOCAL day of this instant, so an evening approval on the US west\ncoast does not start the store a day early. Defaults to now when omitted.\n",
            "example": "2026-09-09T18:22:00Z"
          },
          "approved_by": {
            "type": "string",
            "maxLength": 300,
            "description": "Who approved it on your side. Opaque to us; never resolved against our own accounts."
          }
        }
      },
      "ApprovalMode": {
        "type": "string",
        "enum": [
          "auto",
          "sample_review"
        ],
        "default": "auto",
        "description": "How creators are approved for this store. `auto` (the default) books matching creators\nstraight away. `sample_review` means the merchant first approves a batch of 5 sample\ncreators (`GET /locations/{partner_location_id}/creator-samples`); nothing starts until\nthat decision.\n"
      },
      "StoreTimezone": {
        "type": "string",
        "maxLength": 64,
        "description": "The store's IANA time zone, `Area/Location` (for example `America/Los_Angeles`). When set it\nwins over the zone we derive from the address. Fixed offsets (`+05:00`), `UTC` and\nabbreviations (`PST`) are refused with `400 validation_error`, `field: timezone`: a\nrestaurant's clock needs daylight saving rules.\n",
        "example": "America/Los_Angeles"
      },
      "OpeningHours": {
        "type": "object",
        "additionalProperties": false,
        "minProperties": 1,
        "description": "The store's week from your POS, venue-local. Keys `mon` to `sun`; each is a list of\n`[\"HH:MM\", \"HH:MM\"]` open and close pairs (24 hour), so an afternoon break is two pairs.\nA close must be later than its open on the same day; `24:00` means open until midnight,\nand a later close goes on the next day as `[\"00:00\", ...]`. A day you leave out, or send as\n`[]`, is CLOSED. Overlapping pairs are merged. At least one open pair is required.\n\nCreators are offered times inside these hours with our house rules on top (no Saturday,\nFriday until 17:00, Sunday from 15:00). A week that is only open when those rules allow no\nvisit (for example Saturday only) is stored, and the store reports `hours.state: missing`\nwith `source: partner` until the hours change.\n",
        "properties": {
          "mon": {
            "$ref": "#/components/schemas/OpeningDay"
          },
          "tue": {
            "$ref": "#/components/schemas/OpeningDay"
          },
          "wed": {
            "$ref": "#/components/schemas/OpeningDay"
          },
          "thu": {
            "$ref": "#/components/schemas/OpeningDay"
          },
          "fri": {
            "$ref": "#/components/schemas/OpeningDay"
          },
          "sat": {
            "$ref": "#/components/schemas/OpeningDay"
          },
          "sun": {
            "$ref": "#/components/schemas/OpeningDay"
          }
        },
        "example": {
          "mon": [
            [
              "11:00",
              "15:00"
            ],
            [
              "17:00",
              "21:30"
            ]
          ],
          "tue": [
            [
              "11:00",
              "21:30"
            ]
          ],
          "sun": []
        }
      },
      "OpeningDay": {
        "type": "array",
        "maxItems": 6,
        "items": {
          "type": "array",
          "minItems": 2,
          "maxItems": 2,
          "items": {
            "type": "string",
            "pattern": "^(([01][0-9]|2[0-3]):[0-5][0-9]|24:00)$"
          }
        }
      },
      "MerchantContact": {
        "type": "object",
        "additionalProperties": false,
        "minProperties": 1,
        "description": "The person at the restaurant. `email` receives our store notices (instead of your account\nowner); `phone` receives the visit-day text message. Never shown to creators.\n",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 100,
            "nullable": true,
            "example": "Alex Kim"
          },
          "phone": {
            "type": "string",
            "maxLength": 20,
            "nullable": true,
            "description": "E.164, for example `+14155550100`. Anything else is refused with `field: merchant_contact.phone`.",
            "example": "+14155550100"
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 100,
            "nullable": true,
            "example": "owner@example.com"
          }
        }
      },
      "Address": {
        "type": "object",
        "additionalProperties": false,
        "description": "Stored as one line, `line1, city, state postal_code, country`. The store's time zone is\nderived from it, so `state` and `postal_code` are worth sending even where they feel\noptional.\n",
        "properties": {
          "line1": {
            "type": "string",
            "maxLength": 300,
            "nullable": true
          },
          "city": {
            "type": "string",
            "maxLength": 120,
            "nullable": true
          },
          "state": {
            "type": "string",
            "maxLength": 120,
            "nullable": true
          },
          "postal_code": {
            "type": "string",
            "maxLength": 40,
            "nullable": true
          },
          "country": {
            "type": "string",
            "maxLength": 120,
            "nullable": true
          }
        }
      },
      "Plan": {
        "type": "object",
        "additionalProperties": false,
        "description": "What the merchant bought. We store your numbers as you sent them AND the total they imply\n(`videos_per_month` x `term_months`), because 2 x 3 and 6 x 1 are the same total and are\nnot the same contract, and reconciliation is held in your units.\n\nThe standard plan is 2 to 10 videos a month and a term of 3, 8 or 12 months (longer is\nallowed). A plan below 2 videos a month, or below 3 months, is NOT refused: it is accepted\nand carries the plan surcharge on its price (`pricing.plan_surcharge`).\n\nA value outside the ranges below is refused with `400 validation_error` and `field` set to\n`plan.videos_per_month` or `plan.term_months`. Only what a request sends is checked: a\nstore stored earlier with other values keeps working, and a PATCH of another field does not\nhave to restate its plan.\n",
        "properties": {
          "videos_per_month": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10,
            "description": "The merchant's choice, a whole number from 1 to 10 (2 to 10 is standard; 1 adds the plan surcharge). Paced per month (see Monthly pacing).",
            "example": 2
          },
          "term_months": {
            "type": "integer",
            "minimum": 1,
            "maximum": 120,
            "description": "The contract length in months, 1 to 120 (3 or more is standard; below 3 adds the plan surcharge). The collaboration end date is the start date plus this many months.",
            "example": 3
          }
        }
      },
      "Money": {
        "type": "object",
        "required": [
          "per_video",
          "per_month",
          "per_season"
        ],
        "properties": {
          "per_video": {
            "type": "number",
            "example": 100
          },
          "per_month": {
            "type": "number",
            "nullable": true,
            "description": "per_video x videos per month. Null for a store with no plan.",
            "example": 200
          },
          "per_season": {
            "type": "number",
            "nullable": true,
            "description": "per_month x 3.",
            "example": 600
          }
        }
      },
      "Billed": {
        "type": "object",
        "description": "What we bill the partner per published video. Not a fixed price.",
        "required": [
          "fee_per_video",
          "creator_cost_per_video",
          "billed_as"
        ],
        "properties": {
          "fee_per_video": {
            "type": "number",
            "example": 10
          },
          "creator_cost_per_video": {
            "type": "object",
            "nullable": true,
            "description": "Estimate range of the creator cost per video (on a quote, and on a store with no locked price; null on a locked store). The bill uses the actual cost.",
            "properties": {
              "min": {
                "type": "number",
                "example": 20
              },
              "max": {
                "type": "number",
                "example": 40
              }
            }
          },
          "billed_as": {
            "type": "string",
            "enum": [
              "creator_cost_plus_fee"
            ]
          }
        }
      },
      "Pricing": {
        "type": "object",
        "description": "Finished numbers, ready to show. Amounts are in `currency` units.",
        "required": [
          "currency",
          "billing_unit",
          "creatorain",
          "surcharge",
          "plan_surcharge",
          "merchant",
          "season_videos",
          "note"
        ],
        "properties": {
          "currency": {
            "type": "string",
            "example": "USD"
          },
          "billing_unit": {
            "type": "string",
            "enum": [
              "published_video"
            ]
          },
          "creatorain": {
            "$ref": "#/components/schemas/Billed"
          },
          "surcharge": {
            "type": "object",
            "properties": {
              "per_video": {
                "type": "number",
                "example": 10
              }
            }
          },
          "plan_surcharge": {
            "type": "object",
            "description": "The non-standard plan line, included in `merchant`. 0 / false for a standard plan.",
            "properties": {
              "per_video": {
                "type": "number",
                "example": 5
              },
              "applies": {
                "type": "boolean",
                "example": false
              },
              "placeholder": {
                "type": "boolean",
                "description": "True while the amount is provisional.",
                "example": true
              }
            }
          },
          "merchant": {
            "$ref": "#/components/schemas/Money"
          },
          "season_videos": {
            "type": "integer",
            "nullable": true,
            "example": 6
          },
          "note": {
            "type": "string"
          }
        }
      },
      "PriceQuote": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Pricing"
          },
          {
            "type": "object",
            "required": [
              "valid_until"
            ],
            "properties": {
              "valid_until": {
                "type": "string",
                "format": "date",
                "example": "2026-10-31"
              }
            }
          }
        ]
      },
      "StorePricing": {
        "nullable": true,
        "description": "The store's price for the month it is in: the same block as the quote, LOCKED when the store\nwas confirmed. A change to `plan.videos_per_month` or `approval_mode` re-locks it from the\nnext month (at the base price and your surcharge at that moment), shown meanwhile as `upcoming`.\nA later surcharge change does not move it. A store confirmed before pricing existed has no\nlocked price: its block is worked out from the current default terms and marked\n`locked: false`, `source: default` (nothing is stored for it, and it shows the default\ncreator cost range). NULL only when pricing is not set up at all.\n\nRead with a merchant's store token, only the merchant price is returned (`creatorain` and\n`surcharge` are left out).\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/Pricing"
          },
          {
            "type": "object",
            "properties": {
              "locked_at": {
                "type": "string",
                "nullable": true,
                "description": "When the price in force was locked (UTC). Null when `locked` is false."
              },
              "locked": {
                "type": "boolean",
                "description": "False for a store with no locked price, priced from the current defaults."
              },
              "source": {
                "type": "string",
                "enum": [
                  "locked",
                  "default"
                ]
              },
              "upcoming": {
                "type": "object",
                "nullable": true,
                "description": "The price from `from_month` (the store's next month), when a re-lock is waiting.",
                "properties": {
                  "from_month": {
                    "type": "string",
                    "example": "2026-11"
                  },
                  "currency": {
                    "type": "string"
                  },
                  "creatorain": {
                    "$ref": "#/components/schemas/Billed"
                  },
                  "surcharge": {
                    "type": "object",
                    "properties": {
                      "per_video": {
                        "type": "number"
                      }
                    }
                  },
                  "plan_surcharge": {
                    "type": "object",
                    "properties": {
                      "per_video": {
                        "type": "number"
                      },
                      "applies": {
                        "type": "boolean"
                      },
                      "placeholder": {
                        "type": "boolean"
                      }
                    }
                  },
                  "merchant": {
                    "$ref": "#/components/schemas/Money"
                  },
                  "season_videos": {
                    "type": "integer",
                    "nullable": true
                  }
                }
              }
            }
          }
        ]
      },
      "PartnerSettings": {
        "type": "object",
        "required": [
          "surcharge_per_video",
          "currency",
          "updated_at"
        ],
        "properties": {
          "surcharge_per_video": {
            "type": "number",
            "example": 10
          },
          "currency": {
            "type": "string",
            "example": "USD"
          },
          "updated_at": {
            "type": "string",
            "nullable": true,
            "example": "2026-10-01T09:00:00Z"
          }
        }
      },
      "Location": {
        "type": "object",
        "description": "The store as we hold it. ONE body for every verb on a location: the create's 201 (also\nreturned when a terminated store is added again), an identical repeat's 200 (plus a `warning`), the `PATCH` response, and pause, resume and terminate. Write one\nreceiver for it. `GET /locations/{partner_location_id}` returns this plus progress and the\nnext visit (`LocationState`).\n",
        "properties": {
          "location_id": {
            "type": "string",
            "format": "uuid",
            "description": "OUR id for the store. Stable, and what every other endpoint refers to it by."
          },
          "partner_location_id": {
            "type": "string",
            "description": "Your id, as you sent it (trimmed).",
            "example": "store_123"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused",
              "terminated"
            ],
            "description": "`paused` means no NEW scheduling: creators are not offered the store and a new booking\nis refused, while bookings already confirmed are untouched. Resume opens it again.\n`terminated` means the contract has ended. Terminated wins over paused.\n"
          },
          "network": {
            "type": "string",
            "nullable": true,
            "description": "A display label for the local creator pool, for example `example-pool`. Label only.",
            "example": "example-pool"
          },
          "dashboard_url": {
            "type": "string",
            "nullable": true,
            "description": "The embeddable merchant dashboard for this store, carrying a freshly minted 30 minute\nread-only token. Null when this deployment hosts no embedded dashboard, rather than a\nplausible URL pointing at nothing: an iframe rendered from a dead URL shows a merchant\na blank panel, which is worse than a field you can branch on. Mint a fresh one with\n`POST /locations/{partner_location_id}/dashboard-token` when you render the embed.\n"
          },
          "timezone": {
            "type": "string",
            "nullable": true,
            "description": "IANA name for the store as we resolve it: the `timezone` you sent when you sent one,\notherwise derived from the address. Null when neither yields one.\n",
            "example": "America/Los_Angeles"
          },
          "created_at": {
            "type": "string",
            "nullable": true,
            "description": "RFC 3339 with the store's offset.",
            "example": "2026-09-10T13:22:00-07:00"
          },
          "hours": {
            "type": "object",
            "required": [
              "state",
              "source"
            ],
            "description": "Whether we hold this store's real opening hours, and where they came from. A store with\nno hours is still `active`: the contract exists and nothing about the enrolment failed.\nWhat it cannot do yet is take a creator booking, because our two date pickers are built\nfrom the stored week and an absent one disables both. `state: missing` is a request to\nchase the hours at source, which is the one place they actually exist.\n",
            "properties": {
              "state": {
                "type": "string",
                "enum": [
                  "missing",
                  "found"
                ]
              },
              "source": {
                "type": "string",
                "nullable": true,
                "description": "Where the hours came from. `partner` means you sent them (`opening_hours`); the rest\nare our own lookups or an operator (`manual`). Null for a store never checked.\n",
                "enum": [
                  "partner",
                  "places",
                  "gemini",
                  "website",
                  "manual",
                  "none"
                ]
              }
            }
          },
          "approval_mode": {
            "$ref": "#/components/schemas/ApprovalMode"
          },
          "pricing": {
            "$ref": "#/components/schemas/StorePricing"
          },
          "first_visit_business_days": {
            "type": "integer",
            "nullable": true,
            "description": "The first-visit commitment for this store, in business days (Monday to Friday, store\nclock, skipping Thanksgiving Day, Christmas Day, New Year's Day and any weekday the\nstore's own opening hours say it is closed), as agreed in your contract. Locked when the store is\ncreated. Null for a store created before this field existed. Show it as given.\n"
          },
          "first_visit_due_by": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "example": "2026-10-06",
            "description": "The store-local date the first creator visit is due by: `first_visit_business_days`\ncounted from the day after the confirm day (`approved_at` when you sent it, otherwise\nthe day the store was created). Locked at creation.\n"
          },
          "rules": {
            "type": "array",
            "description": "WHAT THIS STORE ASKS OF A CREATOR: the terms you set with `PATCH`, read back. Always\npresent, ordered by attribute then operator, and `[]` for a store that asks nothing -\nwhich is what a store has until you send its first rule.\n\nTHIS IS THE SAME ARRAY `PATCH` ACCEPTS, so the obvious read-modify-write is safe: read\na store, change one entry, send the array back. `value` is returned in the shape we\nSTORED it, which is the normalised one - a bare `20000` you sent for `follower_count`\nreads back as `{\"count\": 20000, \"platform\": null}`, and sending that object again\nchanges nothing.\n\nIt is deliberately NOT our internal effective rule set. Terms an operator set on our\nown dashboard through the older per-venue columns are not folded in here, because you\ncan neither send nor clear those through this API and a term you cannot remove is worse\nthan one you cannot see. Everything this array contains, `clear_rules` can remove.\n",
            "items": {
              "$ref": "#/components/schemas/MatchingRule"
            },
            "example": [
              {
                "attribute": "follower_count",
                "operator": "gte",
                "value": {
                  "count": 20000,
                  "platform": "tiktok"
                }
              },
              {
                "attribute": "platform",
                "operator": "eq",
                "value": "tiktok"
              }
            ]
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "The stable value to branch on.",
                "enum": [
                  "unauthorized",
                  "forbidden_ip",
                  "rate_limited",
                  "validation_error",
                  "location_already_exists",
                  "location_not_found",
                  "location_terminated",
                  "reservation_not_found",
                  "conflict",
                  "internal_error"
                ]
              },
              "message": {
                "type": "string",
                "description": "A human sentence. Copy, not contract; it may be reworded."
              },
              "field": {
                "type": "string",
                "description": "The request field at fault, dotted for a nested one (`plan.term_months`)."
              }
            }
          }
        }
      },
      "PatchLocationRequest": {
        "type": "object",
        "minProperties": 1,
        "additionalProperties": false,
        "description": "Every field optional, at least one required. `plan_tier` is deliberately absent: we hold no\ntier per store, and an unknown field is refused by name rather than accepted and dropped.\n",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 300
          },
          "address": {
            "$ref": "#/components/schemas/Address"
          },
          "plan": {
            "$ref": "#/components/schemas/Plan"
          },
          "signature_dishes": {
            "type": "array",
            "maxItems": 50,
            "items": {
              "type": "object",
              "required": [
                "name"
              ],
              "additionalProperties": false,
              "properties": {
                "name": {
                  "type": "string",
                  "maxLength": 300
                },
                "image_url": {
                  "type": "string",
                  "format": "uri",
                  "maxLength": 2000,
                  "nullable": true
                }
              }
            }
          },
          "preferred_language": {
            "type": "string",
            "enum": [
              "zh",
              "en"
            ]
          },
          "approval_mode": {
            "$ref": "#/components/schemas/ApprovalMode"
          },
          "timezone": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StoreTimezone"
              }
            ],
            "nullable": true,
            "description": "Send `null` to clear it; the store goes back to the zone read from its address."
          },
          "opening_hours": {
            "$ref": "#/components/schemas/OpeningHours"
          },
          "merchant_contact": {
            "$ref": "#/components/schemas/MerchantContact"
          },
          "rules": {
            "type": "array",
            "maxItems": 24,
            "description": "Which creators this store will take. REPLACES the store's rules for the attributes\npresent and leaves the rest alone.\n",
            "items": {
              "$ref": "#/components/schemas/MatchingRule"
            }
          },
          "clear_rules": {
            "type": "array",
            "maxItems": 12,
            "description": "Attribute names to remove from this store outright. Needed because an empty `rules`\narray names no attribute and so cannot say what it is clearing.\n",
            "items": {
              "type": "string",
              "enum": [
                "follower_count",
                "platform",
                "language",
                "home_metro",
                "radius_miles",
                "performance_index",
                "engagement_rate"
              ]
            },
            "example": [
              "follower_count"
            ]
          }
        }
      },
      "MatchingRule": {
        "type": "object",
        "required": [
          "attribute",
          "operator",
          "value"
        ],
        "additionalProperties": false,
        "description": "One term this store asks of a creator.\n\n`ethnicity`, `age_band` and `gender` are real members of our attribute list and are\nREFUSED with `422 validation_error` on both this API and our dashboard. Filtering paid work\nby a protected characteristic needs a written policy agreed with counsel, and until one\nexists we will not store the rule.\n\n`follower_count` takes `gte` or `lte` and a value of `{\"count\": N}`, optionally with\n`\"platform\"` to say WHERE the count is measured - `{\"count\": 20000, \"platform\": \"tiktok\"}`\nmeans twenty thousand on TikTok, not twenty thousand in total from somebody who also has a\nTikTok handle. `platform` and `language` take `eq` with one value or `in` with a list; over\na creator who works on several, `eq` means \"this is one of hers\", never \"this is her only\none\".\n\n`home_metro` (local creators only) takes `in`; `radius_miles` (within N miles of this\nstore) takes `lte` and the miles, `25` or `{\"miles\": 25}`, from 1 to 500. Both are measured\nfrom this store's own address, which we resolve when you write the rule and again when the\naddress moves; an address we cannot place in a US city is a `422`.\n\n`performance_index` (content performance score, 0 to 100) and `engagement_rate` (percent,\n`3.5` means 3.5%) take `gte` and a number. They are floors.\n\nA creator we hold no answer for is decided by the store's own policy, which passes by\ndefault - so adding a `language` rule today records your intent and excludes nobody until\nwe hold languages. The follower bar, the platform, `home_metro`, `radius_miles`,\n`performance_index` and `engagement_rate` are the exception and always exclude a creator we\ncannot measure.\n",
        "properties": {
          "attribute": {
            "type": "string",
            "enum": [
              "follower_count",
              "platform",
              "language",
              "home_metro",
              "radius_miles",
              "performance_index",
              "engagement_rate"
            ]
          },
          "operator": {
            "type": "string",
            "enum": [
              "gte",
              "lte",
              "in",
              "eq"
            ]
          },
          "value": {
            "description": "A number, an object, a string or a list of strings, per the attribute.",
            "oneOf": [
              {
                "type": "integer"
              },
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "count": {
                    "type": "integer",
                    "minimum": 0
                  },
                  "platform": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "instagram",
                      "tiktok",
                      "youtube",
                      "facebook"
                    ]
                  }
                }
              }
            ]
          }
        }
      },
      "PauseRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "reason": {
            "type": "string",
            "maxLength": 500,
            "nullable": true,
            "description": "Why, in your words. Kept on the store so an operator reading it later can tell a partner\npause from one of ours. Defaults to `Paused by partner`.\n",
            "example": "Kitchen refit"
          }
        }
      },
      "TerminateRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "effective_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the CONTRACT ends, RFC 3339. May be in the future or the past. Defaults to now.\nDifferent from when you told us, which we record separately.\n",
            "example": "2026-10-31T00:00:00Z"
          },
          "reason": {
            "type": "string",
            "maxLength": 500,
            "nullable": true,
            "example": "Merchant closed"
          }
        }
      },
      "LocationState": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Location"
          },
          {
            "type": "object",
            "properties": {
              "progress": {
                "type": "object",
                "description": "This store's progress against its plan.",
                "properties": {
                  "season_target": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Videos per SEASON (three months): `videos_per_month` x 3, following the plan in\nforce for each month of the current season (a PATCH applies from the next\nmonth). Null when the store has no plan.\n",
                    "example": 6
                  },
                  "quarter_target": {
                    "type": "integer",
                    "nullable": true,
                    "deprecated": true,
                    "description": "DEPRECATED. The old name of `season_target`, sent for one more release with the\nsame value. Read `season_target`. Before this release it held the whole-term\ntotal.\n",
                    "example": 6
                  },
                  "published_count": {
                    "type": "integer",
                    "description": "Videos filmed at this store that are live.",
                    "example": 2
                  },
                  "booked_count": {
                    "type": "integer",
                    "deprecated": true,
                    "description": "Confirmed visits at this store that are still in the future. Same value as\n`booked_upcoming`, kept so existing readers do not break; new code should read\n`booked_upcoming`. NOT the same as `pipeline.booked`, which counts every creator\nwho ever booked this season, including visits already done.\n",
                    "example": 1
                  },
                  "booked_upcoming": {
                    "type": "integer",
                    "description": "Confirmed visits at this store that are still in the future (upcoming only; done,\ncancelled and past visits are not counted). `pipeline.booked` is the season total\nof creators who booked, so it is normally the larger of the two.\n",
                    "example": 1
                  }
                }
              },
              "next_visit": {
                "nullable": true,
                "description": "The next confirmed creator visit, or null when none is booked.",
                "type": "object",
                "properties": {
                  "reservation_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "arrive_at": {
                    "type": "string",
                    "nullable": true,
                    "description": "RFC 3339 with the STORE'S offset. The digits are the restaurant's wall clock.",
                    "example": "2026-09-14T19:00:00-07:00"
                  },
                  "timezone": {
                    "type": "string",
                    "nullable": true,
                    "example": "America/Los_Angeles"
                  }
                }
              },
              "notices": {
                "type": "object",
                "description": "How the merchant at this store is reached by email. TWO BOOLEANS AND NO ADDRESS: the\naddress a notice goes to is an account holder's, and this body can be read with a\nstore dashboard token, which is a merchant's browser credential.\n",
                "properties": {
                  "contact_present": {
                    "type": "boolean",
                    "description": "Whether there is anybody to write to at all. False means nothing we send about\nthis store's bookings reaches a human, which is worth seeing before a creator\nturns up rather than afterwards.\n"
                  },
                  "single_channel": {
                    "type": "boolean",
                    "description": "True when the address the notices go to IS the store's own published inbox: one\ninbox doing two jobs. It matters because a filter, a holiday auto-responder or a\nbounce then takes out the booking notice and the marketing conversation\ntogether, so the visit-day fallback has to be a different channel rather than a\nsecond mail.\n"
                  }
                }
              },
              "blocking_reasons": {
                "type": "array",
                "description": "What stops this store from recruiting creators. Empty when recruiting can run. A\nstore recruits only when the campaign is running, its opening hours are known and\nit is not paused or terminated. A paused store reports `store_paused` (resume it and\nit clears). A terminated store reports `store_terminated` and never `store_paused`:\nits contract is over and it cannot be resumed. When recruiting starts we send the\n`location.recruiting` webhook once, with `campaign_id` and `first_visit_due_by`.\n",
                "items": {
                  "type": "string",
                  "enum": [
                    "hours_missing",
                    "campaign_not_running",
                    "store_paused",
                    "store_terminated"
                  ]
                },
                "example": [
                  "hours_missing"
                ]
              },
              "pipeline": {
                "type": "object",
                "description": "This season's creator pipeline for the store, as counts. Cumulative: each creator\nis counted once, at her furthest stage, in that stage and every earlier one, so\ncontacted >= interested >= accepted >= booked >= visited >= posted. `declined`\n(said no, or no answer after follow-ups) is separate and also inside `contacted`.\nContacted and interested count creators in the store's metro area; accepted and\nlater need a signature, booking or video at THIS store.\n",
                "properties": {
                  "contacted": {
                    "type": "integer",
                    "example": 38
                  },
                  "interested": {
                    "type": "integer",
                    "example": 7
                  },
                  "accepted": {
                    "type": "integer",
                    "example": 3
                  },
                  "booked": {
                    "type": "integer",
                    "description": "Creators who booked a visit here this season, counted once each at their furthest\nstage, done visits included. Not the same as `progress.booked_upcoming`, which is\nonly the confirmed visits still to come.\n",
                    "example": 1
                  },
                  "visited": {
                    "type": "integer",
                    "example": 0
                  },
                  "posted": {
                    "type": "integer",
                    "example": 0
                  },
                  "declined": {
                    "type": "integer",
                    "example": 4
                  }
                }
              },
              "results": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SeasonResults"
                  }
                ],
                "nullable": true,
                "description": "This season's results (guide 8.1 to 8.4). Updates with each weekly reading of the\nvideos. Null when the store has no season yet.\n"
              },
              "previous_season_results": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SeasonResults"
                  }
                ],
                "nullable": true,
                "description": "The season before this one. Its final reading comes 2 weeks after it ends; from\nthen on `final` is true and the block never changes. Null in the first season.\n"
              }
            }
          }
        ]
      },
      "SeasonResults": {
        "type": "object",
        "description": "One season of one store: three calendar months on the store's clock. A video is one creator\nvisit, counted once even when posted on both Instagram and TikTok. Plays and averages are\nover the posts that have been read. Averages are per post. Shares exist on TikTok only.\n",
        "properties": {
          "season_starts_on": {
            "type": "string",
            "format": "date",
            "example": "2026-10-01"
          },
          "season_ends_on": {
            "type": "string",
            "format": "date",
            "description": "Last day of the season.",
            "example": "2026-12-31"
          },
          "season_videos_published": {
            "type": "integer",
            "example": 3
          },
          "season_videos_target": {
            "type": "integer",
            "nullable": true,
            "example": 9
          },
          "season_posts": {
            "type": "integer",
            "description": "Posts behind those videos (a cross-post is 2 posts, 1 video).",
            "example": 4
          },
          "season_plays": {
            "type": "integer",
            "example": 58400
          },
          "posts_read": {
            "type": "integer",
            "description": "The season's posts that have been read: a reading has written their numbers. At most\n`season_posts`; the rest have not been read yet, so they count toward no play or average.\n",
            "example": 4
          },
          "season_plays_posts_measured": {
            "type": "integer",
            "description": "The read posts whose plays are measured, the posts `season_plays` adds up. Below\n`posts_read` when a platform did not publish a post's plays. With 0, `season_plays` is\n0 because nothing was counted, not because the videos got no plays.\n",
            "example": 4
          },
          "average_likes_per_video": {
            "type": "integer",
            "nullable": true,
            "example": 785
          },
          "average_shares_per_video": {
            "type": "integer",
            "nullable": true,
            "description": "Over TikTok posts only. Null with none.",
            "example": 106
          },
          "average_creator_followers": {
            "type": "integer",
            "nullable": true,
            "example": 13070
          },
          "estimated_new_guests": {
            "type": "integer",
            "description": "Estimated groups of guests who came because of the videos. Our own model, from each\npost's plays, likes, comments, shares and saves and the share of the creator's\nfollowers who are local to the store. Updates weekly, fixed at the final reading.\nAn estimate, not a commitment. Show it with a (?) and `estimated_new_guests_help`.\n",
            "example": 24
          },
          "estimated_new_guests_note": {
            "type": "string",
            "example": "Estimate from video engagement data. Not a commitment."
          },
          "estimated_new_guests_help": {
            "type": "string",
            "description": "Tooltip text for the (?) icon."
          },
          "commitment": {
            "type": "object",
            "description": "The season commitment check (guide 8.4). Status is `met`, `not_met` or `pending`. Before\nthe final reading a check is `met` once reached (provisional for averages) or `pending`;\nat the final reading `met` or `not_met`. Average shares is `not_measured` at the final\nreading when no post of the season publishes shares. First visit compares the first\nvisit that has happened with `first_visit_due_by`.\n",
            "properties": {
              "videos": {
                "$ref": "#/components/schemas/CommitmentCheck"
              },
              "average_creator_followers": {
                "$ref": "#/components/schemas/CommitmentCheck"
              },
              "season_plays": {
                "$ref": "#/components/schemas/CommitmentCheck"
              },
              "average_likes_per_video": {
                "$ref": "#/components/schemas/CommitmentCheck"
              },
              "average_shares_per_video": {
                "$ref": "#/components/schemas/CommitmentCheck"
              },
              "first_visit": {
                "type": "object",
                "properties": {
                  "value": {
                    "type": "string",
                    "format": "date",
                    "nullable": true,
                    "example": "2026-10-03"
                  },
                  "target": {
                    "type": "string",
                    "format": "date",
                    "nullable": true,
                    "example": "2026-10-06"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "met",
                      "not_met",
                      "pending"
                    ]
                  }
                }
              }
            }
          },
          "measured_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2026-11-09T02:00:00Z"
          },
          "final": {
            "type": "boolean",
            "example": false
          }
        }
      },
      "CommitmentCheck": {
        "type": "object",
        "properties": {
          "value": {
            "type": "integer",
            "nullable": true,
            "example": 58400
          },
          "target": {
            "type": "integer",
            "nullable": true,
            "example": 60000
          },
          "status": {
            "type": "string",
            "enum": [
              "met",
              "not_met",
              "pending",
              "not_measured"
            ]
          }
        }
      },
      "StoreCreator": {
        "type": "object",
        "description": "One named creator of a store. Public profile only.",
        "properties": {
          "handle": {
            "type": "string",
            "nullable": true,
            "example": "example_creator"
          },
          "platforms": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "instagram",
              "tiktok"
            ]
          },
          "followers": {
            "type": "integer",
            "nullable": true,
            "example": 18200
          },
          "city": {
            "type": "string",
            "nullable": true,
            "example": "San Jose"
          },
          "stage": {
            "type": "string",
            "enum": [
              "accepted",
              "booked",
              "visited",
              "posted"
            ]
          },
          "next_visit": {
            "type": "string",
            "nullable": true,
            "description": "Her next confirmed visit at this store, RFC 3339 with the store's offset.",
            "example": "2026-10-03T15:00:00-07:00"
          }
        }
      },
      "CreatorSamples": {
        "type": "object",
        "required": [
          "batch_id",
          "batch_number",
          "status",
          "creators"
        ],
        "properties": {
          "batch_id": {
            "type": "string",
            "nullable": true,
            "example": "smp_1b4e28ba-2fa1-4d3b-a3f5-ef19b5a7633b"
          },
          "batch_number": {
            "type": "integer",
            "nullable": true,
            "example": 1
          },
          "status": {
            "type": "string",
            "enum": [
              "preparing",
              "ready",
              "approved"
            ]
          },
          "prepared_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "decided_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "summary": {
            "type": "object",
            "nullable": true,
            "properties": {
              "followers_min": {
                "type": "integer",
                "nullable": true,
                "example": 7900
              },
              "followers_max": {
                "type": "integer",
                "nullable": true,
                "example": 24800
              },
              "followers_average": {
                "type": "integer",
                "nullable": true,
                "example": 14380
              },
              "common_topics": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "example": [
                  "food",
                  "travel"
                ]
              },
              "audience": {
                "type": "string",
                "nullable": true,
                "example": "local (San Jose)"
              }
            }
          },
          "creators": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SampleCreator"
            }
          }
        }
      },
      "SampleCreator": {
        "type": "object",
        "description": "One sample creator. Public profile only.",
        "properties": {
          "handle": {
            "type": "string",
            "nullable": true,
            "example": "example_creator_2"
          },
          "platforms": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "instagram",
              "tiktok"
            ]
          },
          "followers": {
            "type": "integer",
            "nullable": true,
            "example": 18200
          },
          "city": {
            "type": "string",
            "nullable": true,
            "example": "San Jose"
          },
          "average_views": {
            "type": "integer",
            "nullable": true,
            "example": 12400
          },
          "topics": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "food"
            ]
          },
          "recent_posts": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "post_url": {
                  "type": "string",
                  "example": "https://www.instagram.com/p/..."
                },
                "views": {
                  "type": "integer",
                  "nullable": true,
                  "example": 12000
                }
              }
            }
          }
        }
      },
      "SampleDecisionRequest": {
        "type": "object",
        "required": [
          "decision"
        ],
        "properties": {
          "decision": {
            "type": "string",
            "enum": [
              "approve",
              "new_batch"
            ]
          },
          "note": {
            "type": "string",
            "maxLength": 500,
            "nullable": true,
            "description": "Optional, with new_batch. What the merchant wants instead.",
            "example": "prefers Chinese-speaking creators"
          }
        }
      },
      "SampleDecision": {
        "type": "object",
        "properties": {
          "batch_id": {
            "type": "string"
          },
          "batch_number": {
            "type": "integer"
          },
          "decision": {
            "type": "string",
            "enum": [
              "approve",
              "new_batch"
            ]
          },
          "note": {
            "type": "string",
            "nullable": true
          },
          "decided_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "next": {
            "type": "string",
            "enum": [
              "recruiting",
              "new_batch_preparing"
            ]
          }
        }
      },
      "StoreCreatorPage": {
        "type": "object",
        "required": [
          "items",
          "next_cursor"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StoreCreator"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "DashboardToken": {
        "type": "object",
        "required": [
          "token",
          "expires_at"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "A read-only credential for ONE store. Send it as a Bearer header or as `?token=`."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "30 minutes from minting, in UTC. This is a fact about the credential, not a time at the store."
          },
          "dashboard_url": {
            "type": "string",
            "description": "The embeddable dashboard for this store with the token already in it. OMITTED entirely\nwhen this deployment hosts no embedded dashboard.\n"
          }
        }
      },
      "Creator": {
        "type": "object",
        "description": "As much of the creator as an outside system may see. Their contact details are never\nreturned on this API.\n",
        "properties": {
          "handle": {
            "type": "string",
            "nullable": true,
            "description": "Their public handle.",
            "example": "example_creator"
          },
          "followers": {
            "type": "integer",
            "nullable": true,
            "description": "Cross-platform follower total, not one platform's count.",
            "example": 48213
          },
          "platforms": {
            "type": "array",
            "description": "The platforms they publish on. Always an array, never null.",
            "items": {
              "type": "string"
            },
            "example": [
              "instagram",
              "tiktok"
            ]
          }
        }
      },
      "Reservation": {
        "type": "object",
        "description": "One creator booking. NO PERSONAL DATA: no guest name, email or phone, no internal notes,\nand no free-text party note (those routinely carry dietary and medical detail).\n",
        "properties": {
          "booked_by": {
            "type": "string",
            "enum": [
              "creatorain"
            ],
            "description": "Who made the booking. Every booking on this API is a CreatoRain creator visit, so this\nis always `creatorain`. Also on the reservation webhook events.\n",
            "example": "creatorain"
          },
          "no_charge": {
            "type": "boolean",
            "enum": [
              true
            ],
            "description": "Always `true`: the visit is at no charge to the store (show it as \"CreatoRain, no\ncharge\"). Also on the reservation webhook events.\n",
            "example": true
          },
          "reservation_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "confirmed",
              "rejected",
              "cancelled",
              "completed",
              "no_show"
            ]
          },
          "arrive_at": {
            "type": "string",
            "nullable": true,
            "description": "RFC 3339 with the STORE'S offset. The digits are the restaurant's wall clock, so render\nthem as they are; do not convert.\n",
            "example": "2026-09-14T19:00:00-07:00"
          },
          "timezone": {
            "type": "string",
            "nullable": true,
            "example": "America/Los_Angeles"
          },
          "party_size": {
            "type": "integer",
            "description": "How many people are coming, the creator included. Always a number: 2 (the creator plus\none guest) when the booking does not state one. The same value the webhook events send.\n",
            "example": 2
          },
          "creator": {
            "$ref": "#/components/schemas/Creator"
          },
          "dish": {
            "type": "string",
            "nullable": true,
            "description": "The dish chosen for this booking, or null when none was chosen. Never a guess from the\nstore's menu. The same value the webhook events send.\n"
          },
          "created_at": {
            "type": "string",
            "nullable": true,
            "description": "RFC 3339 with the store's offset."
          },
          "updated_at": {
            "type": "string",
            "nullable": true,
            "description": "RFC 3339 with the store's offset. This is the sort key of the list."
          },
          "acknowledged_at": {
            "type": "string",
            "nullable": true,
            "description": "When a human at the restaurant told us they had seen this booking, RFC 3339 with the\nSTORE'S offset. Null when nobody has, which is what `acknowledged=false` filters on.\n\nTHE EARLIEST ACKNOWLEDGEMENT, not the latest. A booking can carry both kinds (forwarded\nto the manager, and acknowledged), and the question this answers is \"has anybody seen\nthis yet, and when\".\n",
            "example": "2026-09-11T16:04:12-07:00"
          },
          "acknowledged_kind": {
            "type": "string",
            "nullable": true,
            "enum": [
              "forwarded_to_manager",
              "acknowledged_self"
            ],
            "description": "Which kind that earliest acknowledgement was. Null when there is none."
          },
          "partner_location_id": {
            "type": "string",
            "description": "Present only on `GET /reservations`, where a row has to say which store it belongs to."
          },
          "location_id": {
            "type": "string",
            "format": "uuid",
            "description": "Present only on `GET /reservations`."
          }
        }
      },
      "ReservationPage": {
        "type": "object",
        "required": [
          "items",
          "next_cursor"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Reservation"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "description": "Send this back as `cursor` for the next page. NULL means this was the last page, and it\nis honest about that: we read one row beyond your `limit` to answer the question, so a\npage that happens to be exactly full does not cost you an extra empty request.\n"
          }
        }
      },
      "Content": {
        "type": "object",
        "description": "One published video filmed at the store.",
        "properties": {
          "content_id": {
            "type": "string",
            "format": "uuid"
          },
          "post_url": {
            "type": "string",
            "nullable": true,
            "example": "https://www.instagram.com/reel/abc123/"
          },
          "platform": {
            "type": "string",
            "nullable": true,
            "example": "instagram"
          },
          "published_at": {
            "type": "string",
            "nullable": true,
            "description": "RFC 3339 with the store's offset.",
            "example": "2026-09-12T17:00:00-07:00"
          },
          "timezone": {
            "type": "string",
            "nullable": true,
            "example": "America/Los_Angeles"
          },
          "is_collab": {
            "type": "boolean",
            "description": "Whether the store's own account is a co-author of the post, read from the post's\nco-author list when the video's numbers are read. `false` when we could not tell,\nwhich includes every TikTok post (that feed has no co-author list) and any post not\nyet read.\n"
          },
          "content_reuse": {
            "type": "boolean",
            "nullable": true,
            "description": "Whether the creator allowed the brand to reuse this content, as she chose it when she\nsigned her agreement. `true` allowed; `false` declined, so the video is for her own\npost only; `null` when no choice was recorded (agreements signed before the option\nexisted).\n"
          },
          "creator": {
            "type": "object",
            "properties": {
              "handle": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "metrics": {
            "nullable": true,
            "description": "NULL until the video's numbers have first been read from the public post. After that\nit is an object, and a number the platform does not publish is null, never 0 (for\nexample Instagram shares and saves).\n",
            "type": "object",
            "properties": {
              "views": {
                "type": "integer",
                "nullable": true,
                "description": "Plays."
              },
              "likes": {
                "type": "integer",
                "nullable": true
              },
              "comments": {
                "type": "integer",
                "nullable": true
              },
              "shares": {
                "type": "integer",
                "nullable": true
              },
              "saves": {
                "type": "integer",
                "nullable": true
              }
            }
          },
          "metrics_measured_at": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MetricsMeasuredAt"
              }
            ],
            "nullable": true,
            "description": "When each number in `metrics` was read. Null exactly when `metrics` is null.\n"
          }
        }
      },
      "MetricsMeasuredAt": {
        "type": "object",
        "description": "When each of a video's five numbers was read from the public post, RFC 3339 in UTC. A key\nis null when that number has never been read, beside a null number, so a measured 0 (a\ntime here) can be told apart from a number nobody counted (null here). Sent beside\n`metrics` on the content feed and beside the five numbers in the `content.metrics_updated`\nwebhook.\n",
        "properties": {
          "views": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2026-11-09T02:00:00Z"
          },
          "likes": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2026-11-09T02:00:00Z"
          },
          "comments": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2026-11-09T02:00:00Z"
          },
          "shares": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": null
          },
          "saves": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": null
          }
        }
      },
      "ContentPage": {
        "type": "object",
        "required": [
          "items",
          "next_cursor"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Content"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "AcknowledgeRequest": {
        "type": "object",
        "required": [
          "kind"
        ],
        "additionalProperties": false,
        "description": "What the merchant did. Two values and no third: this is the whole product question, and a\nvalue we accepted but had no meaning for would leave both sides believing something had\nbeen recorded.\n",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "forwarded_to_manager",
              "acknowledged_self"
            ],
            "description": "`forwarded_to_manager`: they passed the booking to whoever is on the floor.\n`acknowledged_self`: they dealt with it themselves.\n",
            "example": "forwarded_to_manager"
          }
        }
      },
      "Acknowledgement": {
        "type": "object",
        "required": [
          "reservation_id",
          "kind",
          "acknowledged_at",
          "timezone"
        ],
        "description": "What we now hold. On a repeat this is the FIRST acknowledgement's time, not the retry's.\nNo personal data: an acknowledgement says a booking was seen, not who is coming.\n",
        "properties": {
          "reservation_id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "enum": [
              "forwarded_to_manager",
              "acknowledged_self"
            ]
          },
          "acknowledged_at": {
            "type": "string",
            "nullable": true,
            "description": "RFC 3339 with the STORE'S offset, because \"the manager saw this at 4 pm\" only means\nanything at the restaurant.\n",
            "example": "2026-09-11T16:04:12-07:00"
          },
          "timezone": {
            "type": "string",
            "nullable": true,
            "example": "America/Los_Angeles"
          }
        }
      },
      "Summary": {
        "type": "object",
        "required": [
          "active_stores",
          "paused_stores",
          "published_this_month",
          "visits_next_14_days",
          "reminders_acknowledged_this_month",
          "unacknowledged_upcoming",
          "generated_at",
          "timezone"
        ],
        "description": "Your estate as it stands. Counts only: there is no personal data anywhere in this body.\n",
        "properties": {
          "active_stores": {
            "type": "integer",
            "description": "Stores with a live contract that are being offered to creators.",
            "example": 42
          },
          "paused_stores": {
            "type": "integer",
            "description": "Stores with a live contract that are paused: no NEW scheduling, confirmed bookings\nuntouched. Terminated stores are in NEITHER number, so these two do not necessarily add\nup to every store you have ever sent us.\n",
            "example": 3
          },
          "published_this_month": {
            "type": "integer",
            "description": "Videos live on a platform, published inside each store's OWN local month. The same\ndefinition `GET /billing/monthly` uses, so this tile and that invoice cannot differ.\n",
            "example": 17
          },
          "visits_next_14_days": {
            "type": "integer",
            "description": "Confirmed creator visits arriving in the next 14 days, on each store's own wall clock.\nCounted across EVERY store of the key including terminated ones, because ending a\ncontract does not cancel a visit a creator has already been promised.\n",
            "example": 9
          },
          "reminders_acknowledged_this_month": {
            "type": "integer",
            "description": "Bookings a human at a restaurant confirmed seeing this month, counted ONCE each. A\nbooking that was both forwarded to the manager and acknowledged is one booking somebody\ndealt with, not two.\n",
            "example": 12
          },
          "unacknowledged_upcoming": {
            "type": "integer",
            "description": "THE NUMBER TO ACT ON: confirmed visits arriving within 24 hours that nobody at the\nrestaurant has acknowledged. The rows behind it are\n`GET /reservations?status=confirmed&from=<today>&to=<tomorrow>&acknowledged=false`.\n",
            "example": 2
          },
          "generated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this was computed, RFC 3339 in UTC. A fact about our run, not a time at any store.",
            "example": "2026-09-11T09:00:00Z"
          },
          "timezone": {
            "type": "string",
            "enum": [
              "UTC"
            ],
            "description": "The zone of `generated_at`, which is the only time on this body. Always `UTC`. The\nCOUNTS are on each store's own clock; see the endpoint description.\n"
          }
        }
      },
      "BillingRow": {
        "type": "object",
        "description": "One store's month. Counts only: there is no personal data anywhere in this report.\n",
        "properties": {
          "partner_location_id": {
            "type": "string",
            "description": "YOUR id for the store, which is what your own billing is keyed on.",
            "example": "store_123"
          },
          "location_id": {
            "type": "string",
            "format": "uuid",
            "description": "Our id for the same store."
          },
          "videos_published": {
            "type": "integer",
            "description": "Videos filmed at this store that are LIVE and were published inside the month IN THIS\nSTORE'S OWN CLOCK. See the endpoint description for why that matters. Only videos made\non your key's campaign count: a video another campaign made at the same restaurant is\nnot on your invoice.\n",
            "example": 2
          },
          "plan_videos_per_month": {
            "type": "integer",
            "nullable": true,
            "description": "The plan in force in THAT month (a plan change applies from the next month), echoed so an\ninvoice line can be checked without a second call.\n",
            "example": 2
          },
          "plan_term_months": {
            "type": "integer",
            "nullable": true,
            "example": 3
          },
          "reservations_completed": {
            "type": "integer",
            "description": "Visits at this store marked completed, by VISIT DATE in the store's calendar. Only visits booked on your key's campaign count.",
            "example": 3
          },
          "no_shows": {
            "type": "integer",
            "description": "Visits at this store marked no_show, by the same date and the same campaign.",
            "example": 1
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused",
              "terminated"
            ],
            "description": "The store's state at the moment of the read, so a row of zeroes can be explained\nwithout a second call. A terminated store appears only if it had a video or a visit in\nthe month.\n"
          },
          "makeup_videos": {
            "type": "integer",
            "description": "Of `videos_published`, the free replacement videos (after a no-show, or for a video the\nmerchant rejected). Never billed.\n",
            "example": 0
          },
          "billed_videos": {
            "type": "integer",
            "description": "`videos_published` - `makeup_videos`: the videos the two amounts are for.",
            "example": 3
          },
          "cost_pending_videos": {
            "type": "integer",
            "description": "Billed videos whose creator cost is not known yet. While above 0, `amount_due` is null.\n",
            "example": 0
          },
          "currency": {
            "type": "string",
            "nullable": true,
            "description": "The currency of the amounts. Null when the store has no locked price.",
            "example": "USD"
          },
          "amount_due": {
            "type": "number",
            "nullable": true,
            "description": "What CreatoRain bills you for this store this month: the sum over billed videos of\n(that video's creator cost + our locked fee per video). Null for a store with no locked\nprice, or while `cost_pending_videos` is above 0.\n",
            "example": 450
          },
          "merchant_amount": {
            "type": "number",
            "nullable": true,
            "description": "What the merchant owes for the month with your surcharge included, ready for your\ninvoice: `billed_videos` x (the flat merchant price + your locked surcharge). Null for a\nstore with no locked price.\n",
            "example": 1125
          }
        }
      },
      "BillingMonth": {
        "type": "object",
        "required": [
          "month",
          "note",
          "amounts_note",
          "generated_at",
          "items",
          "next_cursor"
        ],
        "properties": {
          "month": {
            "type": "string",
            "description": "The month asked for, echoed back, so a stored report says what it is of.",
            "example": "2026-09"
          },
          "note": {
            "type": "string",
            "description": "The one limitation of these numbers, on every response rather than in a document\nsomebody has to remember to read.\n",
            "example": "content published before 2026-09-09 carries no store and is not counted"
          },
          "amounts_note": {
            "type": "string",
            "description": "When the amounts are null, and that make-up videos are never billed. On every response.",
            "example": "amount_due and merchant_amount are null for a store with no locked price (confirmed before pricing); amount_due is also null while a billed video has no known creator cost (see cost_pending_videos); make-up videos are never billed"
          },
          "generated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this was computed, RFC 3339 in UTC. A fact about our run rather than a time at any\nstore, so it carries no store offset.\n"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BillingRow"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "description": "Send this back as `cursor` for the next page of STORES. The stores are ordered by\n`partner_location_id` ascending rather than by recency, because a reconciliation file\nis diffed against last month's and a stable order is worth more here than newness.\n"
          }
        }
      }
    }
  }
}