{
 "openapi":"3.1.0",
 "info":{"title":"ApiPoi partner read API — pre-launch draft","version":"0.1.0","description":"Documents existing read behaviour, not approved commercial plans. Checkout is disabled. Current meter: every returned POI record, including cached responses. Pricing and credit-based plans are provisional. Licensing clearance and paid launch approval remain pending."},
 "servers":[{"url":"https://app.apipoi.com"}],
 "security":[{"ApiKey":[]}],
 "paths":{
  "/v1/partner/places/nearby":{"get":{
   "operationId":"nearbyPlaces","summary":"Bounded nearby active places",
   "description":"Requires explicit latitude/longitude. Radius defaults to 2000m, maximum 20000m. Existing order uses geometry proximity, while radius filtering uses geography distance. Returns at most 50 places. No paging/total count. Each returned place consumes one record from the account pass; cache hits are charged. Do not assume global data completeness or source freshness.",
   "parameters":[
    {"name":"lat","in":"query","required":true,"schema":{"type":"number","minimum":-90,"maximum":90}},
    {"name":"lon","in":"query","required":true,"schema":{"type":"number","minimum":-180,"maximum":180}},
    {"name":"radius_m","in":"query","schema":{"type":"number","minimum":50,"maximum":20000,"default":2000}},
    {"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":30}},
    {"name":"q","in":"query","description":"Name/address case-insensitive pattern; currently trimmed/truncated to 100 characters. SQL pattern wildcard behaviour is not literal search.","schema":{"type":"string"}},
    {"name":"category","in":"query","description":"Case-insensitive pattern against approved-release source category labels (underscores displayed as spaces), before result limiting; not a category ID. Trimmed/truncated to 100 characters. The legacy catalogue is not a complete list of these labels.","schema":{"type":"string"}}
   ],
   "responses":{
    "200":{"description":"Matching places and charged remaining allowance","content":{"application/json":{"schema":{"type":"object","required":["places","search","access"],"properties":{"places":{"type":"array","maxItems":50,"items":{"$ref":"#/components/schemas/Place"}},"search":{"type":"object"},"access":{"$ref":"#/components/schemas/Access"}}}}}},
    "400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/NoAccess"},"429":{"$ref":"#/components/responses/Limited"},"500":{"$ref":"#/components/responses/Unavailable"},"503":{"$ref":"#/components/responses/Unavailable"}
   }
  }},
  "/v1/partner/places/{id}":{"get":{
   "operationId":"placeDetail","summary":"One active place; costs one returned record",
   "parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],
   "responses":{"200":{"description":"Place and access balance","content":{"application/json":{"schema":{"type":"object","required":["place","access"],"properties":{"place":{"$ref":"#/components/schemas/Place"},"access":{"$ref":"#/components/schemas/Access"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/NoAccess"},"404":{"description":"No active matching place","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/Limited"},"500":{"$ref":"#/components/responses/Unavailable"},"503":{"$ref":"#/components/responses/Unavailable"}}
  }},
  "/v1/partner/categories":{"get":{
   "operationId":"categories","summary":"Enabled categories; zero returned-record charge",
   "description":"Still requires an active read-enabled account/key and available pass. This private-preview legacy shared catalogue is not yet commercially cleared and must not be treated as a release-ready independent taxonomy. Categories do not imply every POI has a complete category assignment.",
   "responses":{"200":{"description":"Category taxonomy","content":{"application/json":{"schema":{"type":"object","required":["categories"],"properties":{"categories":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"display_name":{"type":"string"},"depth":{"type":"integer"},"parent_id":{"type":["string","null"],"format":"uuid"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/NoAccess"},"429":{"$ref":"#/components/responses/Limited"},"500":{"$ref":"#/components/responses/Unavailable"},"503":{"$ref":"#/components/responses/Unavailable"}}
  }}
 },
 "components":{
  "securitySchemes":{"ApiKey":{"type":"apiKey","in":"header","name":"X-ApiPoi-Api-Key","description":"Backend-only secret; never ship in mobile code, public JavaScript, URLs or screenshots."}},
  "schemas":{
   "Place":{"type":"object","required":["id","canonical_name"],"properties":{"id":{"type":"string","format":"uuid"},"canonical_name":{"type":"string"},"address":{"type":["string","null"]},"postcode":{"type":["string","null"]},"country":{"type":["string","null"]},"region":{"type":["string","null"]},"city":{"type":["string","null"]},"categories":{"type":"array","items":{"type":"string"}},"distance_m":{"type":"integer","description":"Nearby response only, rounded metres"},"latitude":{"type":"number","description":"Detail response only"},"longitude":{"type":"number","description":"Detail response only"}}},
   "Access":{"type":"object","required":["expires_at","remaining_records"],"properties":{"expires_at":{"type":"string","format":"date-time"},"remaining_records":{"type":["integer","string"],"description":"PostgreSQL bigint may be serialised as a string"}}},
   "Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string"}}}
  },
  "responses":{
   "BadRequest":{"description":"Invalid query or identifier","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},
   "Unauthorized":{"description":"Missing, invalid, revoked or expired key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},
   "NoAccess":{"description":"Read access unavailable, pass expired or allowance exhausted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},
   "Limited":{"description":"Request/concurrency limit; retry later. Per-key read limit 600/minute; account-wide reads share 600/minute and four concurrent slots across keys. Single-replica in-memory safeguards reset on restart; not final plan entitlements.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},
   "Unavailable":{"description":"Temporary service/database/capacity error; retry with bounded backoff","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}
  }
 }
}
