{
  "openapi": "3.1.0",
  "info": {
    "title": "MapXPet Community Boards API (DRAFT, not live)",
    "version": "0.1.0-draft",
    "description": "Lost pets (free, no auth), breeding (members create/respond: entitlement breeding_board), pets for sale (members see full details/create/message/report: entitlement pets_for_sale). Report threshold confirmed: 3 distinct member reports auto-hide a sale listing pending review (x-report-threshold).",
    "x-moderation": {
      "report_threshold": 3,
      "report_threshold_scope": "all listing types, members and non-members (t223)",
      "appeals": "members and non-members, one per listing (t223)"
    }
  },
  "x-report-threshold": 3,
  "servers": [
    {
      "url": "https://mapxpet.com",
      "description": "Production (NOT LIVE YET: endpoints return 404 until launch)"
    }
  ],
  "paths": {
    "/api/v1/lost-pets.json": {
      "get": {
        "summary": "List public lost and found pet listings (free, no auth)",
        "security": [],
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "description": "Filter by listing type (t212). Omit for both.",
            "schema": {
              "enum": [
                "lost",
                "found"
              ]
            }
          },
          {
            "name": "city",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "species",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "since",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/LostPetPublic"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/lost-pets/{id}.json": {
      "get": {
        "summary": "One public lost-pet listing",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LostPetPublic"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/lost-pets": {
      "post": {
        "summary": "Report a lost or found pet (free, no auth). type=found requires photos (min 1) and found_location. Agents may submit for a human; listing stays pending until the human confirms via emailed link.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LostPetCreate"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted, confirmation email sent. Includes the instant scam-scan decision (scan.status approved|rejected, reason_category).",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "status": {
                          "const": "pending_confirmation"
                        }
                      }
                    },
                    {
                      "type": "object",
                      "properties": {
                        "scan": {
                          "$ref": "#/components/schemas/ScanResult"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "missing_required: a found-pet post (type=found) without at least one photo or without found_location (city + area). Error.fields lists the missing fields, e.g. [\"photos\",\"found_location.area\"].",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/lost-pets/{id}/messages": {
      "post": {
        "summary": "Message the owner through the MapXPet relay (free). Owner contact is never returned.",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MessageCreate"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Relayed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageAccepted"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/lost-pets/{id}/found": {
      "post": {
        "summary": "Mark as found / close listing (owner manage token from email link)",
        "security": [
          {
            "manageToken": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Closed"
          },
          "401": {
            "description": "bad token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/breeding.json": {
      "get": {
        "summary": "List breeding listings (public browse)",
        "security": [],
        "parameters": [
          {
            "name": "species",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "breed",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "city",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/BreedingPublic"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/breeding/{id}.json": {
      "get": {
        "summary": "One breeding listing (public)",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BreedingPublic"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/breeding": {
      "post": {
        "summary": "Create breeding listing (members only, entitlement breeding_board)",
        "security": [
          {
            "memberBearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BreedingCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created. Includes the instant scam-scan decision (scan.status approved|rejected, reason_category).",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/BreedingPublic"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "scan": {
                          "$ref": "#/components/schemas/ScanResult"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "not_member: member bearer token missing (401) or lacks entitlement (403)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not_member: member bearer token missing (401) or lacks entitlement (403)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/breeding/{id}/messages": {
      "post": {
        "summary": "Message breeding owner via relay (members only)",
        "security": [
          {
            "memberBearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MessageCreate"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Relayed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageAccepted"
                }
              }
            }
          },
          "401": {
            "description": "not_member: member bearer token missing (401) or lacks entitlement (403)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not_member: member bearer token missing (401) or lacks entitlement (403)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/breeding/{id}/found": {
      "post": {
        "summary": "Close breeding listing (owner member)",
        "security": [
          {
            "memberBearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Closed"
          },
          "401": {
            "description": "not_member: member bearer token missing (401) or lacks entitlement (403)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not_member: member bearer token missing (401) or lacks entitlement (403)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/pets-for-sale.json": {
      "get": {
        "summary": "List pets for sale: public subset, no seller contact or full details. Hidden/removed listings excluded.",
        "security": [],
        "parameters": [
          {
            "name": "species",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "breed",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "city",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SalePublic"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/pets-for-sale/{id}": {
      "get": {
        "summary": "Full sale listing incl. seller contact (members only, entitlement pets_for_sale). Without a member token returns 401/403 not_member with the public subset.",
        "security": [
          {
            "memberBearer": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Full details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SaleFull"
                }
              }
            }
          },
          "401": {
            "description": "not_member + public subset",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotMemberWithPublic"
                }
              }
            }
          },
          "403": {
            "description": "not_member + public subset",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotMemberWithPublic"
                }
              }
            }
          },
          "404": {
            "description": "not_found or hidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/pets-for-sale": {
      "post": {
        "summary": "Create sale listing (members only)",
        "security": [
          {
            "memberBearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SaleCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created. Includes the instant scam-scan decision (scan.status approved|rejected, reason_category).",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SaleFull"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "scan": {
                          "$ref": "#/components/schemas/ScanResult"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "not_member: member bearer token missing (401) or lacks entitlement (403)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not_member: member bearer token missing (401) or lacks entitlement (403)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/pets-for-sale/{id}/messages": {
      "post": {
        "summary": "Message seller via relay (members only)",
        "security": [
          {
            "memberBearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MessageCreate"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Relayed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageAccepted"
                }
              }
            }
          },
          "401": {
            "description": "not_member: member bearer token missing (401) or lacks entitlement (403)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not_member: member bearer token missing (401) or lacks entitlement (403)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/pets-for-sale/{id}/reports": {
      "post": {
        "summary": "Report a sale listing (members only)",
        "description": "One report per member per listing (409 already_reported). Rule (t208, extended to all boards t223): after 3 reports from 3 distinct members (report_threshold = 3) the listing status becomes hidden_pending_review automatically and enters the admin review queue; admins restore (active) or remove (removed). Reporter identity is never shown to the seller. Admin endpoints (/internal/moderation/*) are internal and not part of this public API.",
        "security": [
          {
            "memberBearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReportCreate"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "report_id": {
                      "type": "string"
                    },
                    "status": {
                      "const": "open"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "not_member: member bearer token missing (401) or lacks entitlement (403)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not_member: member bearer token missing (401) or lacks entitlement (403)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "already_reported",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/listings/{listing_type}/{id}/appeal": {
      "post": {
        "summary": "Ask for a second look at a rejected listing (members or non-members; one appeal per listing)",
        "tags": [
          "Moderation"
        ],
        "security": [
          {
            "memberBearer": []
          },
          {
            "appealLinkToken": []
          }
        ],
        "parameters": [
          {
            "name": "listing_type",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "lost-pets",
                "breeding",
                "pets-for-sale"
              ]
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AppealCreate"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Appeal accepted; listing status appeal_pending",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Appeal"
                }
              }
            }
          },
          "401": {
            "description": "invalid_token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "already_appealed or listing not in rejected state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Decided t223: anyone whose listing is rejected can appeal, including free lost & found posts. Auth: member bearer token, or the signed appeal-link token for non-members. Admin reviews scan results and the listing and makes the final call. 409 already_appealed after the first appeal."
      }
    },
    "/api/v1/photos": {
      "post": {
        "summary": "Upload one listing photo; returns the image check result",
        "tags": [
          "Moderation"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "image/*": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Uploaded and checked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PhotoUploadResult"
                }
              }
            }
          },
          "400": {
            "description": "invalid image",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-provider": {
          "name": "Google Cloud Vision",
          "decided": "t221 (2026-11-01)",
          "request": "one images:annotate call per photo with SAFE_SEARCH_DETECTION, LABEL_DETECTION, WEB_DETECTION",
          "duplicates_internal": "self-hosted perceptual hash (pHash), free",
          "pricing_usd": "first 1,000 units/month free per feature; then SafeSearch $1.50/1k, Label $1.50/1k, Web Detection $3.50/1k; ~$58.50/month at 10,000 photos",
          "account": "the MapXPet Google account used by the social bot",
          "setup_pending_approval": [
            "Cloud project",
            "Vision API enabled",
            "billing account",
            "service-account key stored as a secret, not in repo"
          ]
        }
      }
    },
    "/api/v1/listings/{type}/{id}/interest": {
      "post": {
        "summary": "Send an anonymous \"I'm interested\" ping (humans and AI agents). lost-found: free; no token = email confirm flow. breeding/sale: memberBearer with entitlement breeding_board/pets_for_sale.",
        "security": [
          {},
          {
            "memberBearer": []
          }
        ],
        "x-auth-rules": {
          "lost-found": "none (human.email + email confirm) or memberBearer (skips confirm)",
          "breeding": "memberBearer + breeding_board",
          "sale": "memberBearer + pets_for_sale"
        },
        "parameters": [
          {
            "name": "type",
            "in": "path",
            "required": true,
            "schema": {
              "enum": [
                "lost-found",
                "breeding",
                "sale"
              ]
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "MapXPet-Agent",
            "in": "header",
            "required": false,
            "description": "Informational agent name/version; not used for auth",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InterestCreate"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. status=pending_confirmation (email sent to the human) or pinged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InterestStatus"
                }
              }
            }
          },
          "401": {
            "description": "invalid_token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not_member or blocked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "already_interested",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "email_required / invalid_note",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited (10/day per token, email or IP; 1 per listing per person)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/interests/confirm": {
      "get": {
        "summary": "One-tap email confirm link for lost-found pings (human in the loop). Pings the owner.",
        "security": [],
        "parameters": [
          {
            "name": "t",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Confirmed; HTML page"
          },
          "410": {
            "description": "expired (24 h)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/interests/{interest_id}": {
      "get": {
        "summary": "Status for the pinger or their agent. owner_contact only when shared.",
        "security": [
          {
            "memberBearer": []
          },
          {
            "interestStatusToken": []
          }
        ],
        "parameters": [
          {
            "name": "interest_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "MapXPet-Agent",
            "in": "header",
            "required": false,
            "description": "Informational agent name/version; not used for auth",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InterestStatus"
                }
              }
            }
          },
          "401": {
            "description": "invalid_token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/listings/{type}/{id}/interests": {
      "get": {
        "summary": "Owner: anonymous 'Interested people' list. Owner auth: memberBearer (breeding/sale) or manage token (lost-found).",
        "security": [
          {
            "memberBearer": []
          },
          {
            "manageToken": []
          }
        ],
        "parameters": [
          {
            "name": "type",
            "in": "path",
            "required": true,
            "schema": {
              "enum": [
                "lost-found",
                "breeding",
                "sale"
              ]
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/InterestOwnerRow"
                  }
                }
              }
            }
          },
          "403": {
            "description": "not_owner",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/interests/{interest_id}/share": {
      "post": {
        "summary": "Owner shares chosen contact fields privately with this one pinger (email + API). Stores consent version and timestamp.",
        "security": [
          {
            "memberBearer": []
          },
          {
            "manageToken": []
          }
        ],
        "parameters": [
          {
            "name": "interest_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InterestOwnerRow"
                }
              }
            }
          },
          "403": {
            "description": "not_owner",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "already_resolved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InterestShare"
              }
            }
          }
        }
      }
    },
    "/api/v1/interests/{interest_id}/decline": {
      "post": {
        "summary": "Owner declines; pinger told politely, stays anonymous.",
        "security": [
          {
            "memberBearer": []
          },
          {
            "manageToken": []
          }
        ],
        "parameters": [
          {
            "name": "interest_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InterestOwnerRow"
                }
              }
            }
          },
          "403": {
            "description": "not_owner",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "already_resolved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/interests/{interest_id}/block": {
      "post": {
        "summary": "Owner declines and blocks this pinger from all their listings.",
        "security": [
          {
            "memberBearer": []
          },
          {
            "manageToken": []
          }
        ],
        "parameters": [
          {
            "name": "interest_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InterestOwnerRow"
                }
              }
            }
          },
          "403": {
            "description": "not_owner",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "already_resolved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/interests/{interest_id}/report": {
      "post": {
        "summary": "Owner reports the ping as spam; counts toward moderation, also blocks.",
        "security": [
          {
            "memberBearer": []
          },
          {
            "manageToken": []
          }
        ],
        "parameters": [
          {
            "name": "interest_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InterestOwnerRow"
                }
              }
            }
          },
          "403": {
            "description": "not_owner",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "already_resolved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/listings/{type}/drafts": {
      "post": {
        "summary": "Agent creates a draft listing (status draft_pending_owner). MapXPet emails the human a review link (Approve / Edit / Discard). Nothing reaches moderation until the human approves.",
        "security": [
          {},
          {
            "memberBearer": []
          }
        ],
        "x-auth-rules": {
          "lost-found": "none (human.email required)",
          "breeding": "memberBearer + breeding_board",
          "sale": "memberBearer + pets_for_sale"
        },
        "parameters": [
          {
            "name": "type",
            "in": "path",
            "required": true,
            "schema": {
              "enum": [
                "lost-found",
                "breeding",
                "sale"
              ]
            }
          },
          {
            "name": "MapXPet-Agent",
            "in": "header",
            "required": false,
            "description": "Informational only",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DraftCreate"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Draft created, review email sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DraftStatus"
                }
              }
            }
          },
          "401": {
            "description": "invalid_token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not_member",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "missing_required (e.g. found pet without photo or found_location) / email_required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited (5 drafts/day per token, email or IP)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/drafts/{draft_id}": {
      "get": {
        "summary": "Draft status for the agent (poll or use callback_url)",
        "security": [
          {
            "memberBearer": []
          },
          {
            "interestStatusToken": []
          }
        ],
        "parameters": [
          {
            "name": "draft_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "MapXPet-Agent",
            "in": "header",
            "required": false,
            "description": "Informational only",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DraftStatus"
                }
              }
            }
          },
          "404": {
            "description": "not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/drafts/{draft_id}/review": {
      "post": {
        "summary": "Human review via the signed link in the email (not callable by agents). action=approve submits to photo check + scam scan; the human confirms contact_public and the disclaimer here.",
        "security": [
          {
            "signedReviewLink": []
          }
        ],
        "parameters": [
          {
            "name": "draft_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "action"
                ],
                "properties": {
                  "action": {
                    "enum": [
                      "approve",
                      "discard"
                    ]
                  },
                  "edits": {
                    "type": "object"
                  },
                  "contact_public": {
                    "type": "boolean",
                    "default": false
                  },
                  "public_contact_fields": {
                    "type": "array",
                    "items": {
                      "enum": [
                        "phone",
                        "email"
                      ]
                    }
                  },
                  "contact_consent_text_version": {
                    "type": "string",
                    "description": "Required when contact_public=true"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DraftStatus"
                }
              }
            }
          },
          "410": {
            "description": "expired",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/listings/{listing_type}/{id}/reports": {
      "post": {
        "summary": "Report any listing (lost-pets, breeding, pets-for-sale)",
        "description": "Decided t223: report_threshold = 3 applies to ALL listing types, member and non-member. After 3 reports from 3 distinct members the listing becomes hidden_pending_review until an admin restores (active) or removes (removed) it. One report per member per listing (409 already_reported). Reporter identity is never shown to the owner.",
        "security": [
          {
            "memberBearer": []
          }
        ],
        "parameters": [
          {
            "name": "listing_type",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "lost-pets",
                "breeding",
                "pets-for-sale"
              ]
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReportCreate"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "report_id": {
                      "type": "string"
                    },
                    "status": {
                      "const": "open"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "not_member: member bearer token missing (401) or lacks entitlement (403)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "not_member: member bearer token missing (401) or lacks entitlement (403)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "already_reported",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "not_member",
              "already_reported",
              "rate_limited",
              "invalid",
              "not_found"
            ]
          },
          "message": {
            "type": "string"
          }
        }
      },
      "PublicContact": {
        "type": "object",
        "properties": {
          "phone": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          }
        },
        "additionalProperties": false
      },
      "LostPetPublic": {
        "type": "object",
        "description": "Never contains owner name, exact address or microchip number. Found posts (type=found) never contain microchip numbers or unique markings; they show found_location instead of last_seen_area. public_contact (phone/email) is returned only when contact_public=true.",
        "properties": {
          "id": {
            "type": "string"
          },
          "pet_name": {
            "type": "string"
          },
          "species": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "photos": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "breed_colour": {
            "type": "string"
          },
          "last_seen_area": {
            "type": "string",
            "description": "City + nearest intersection/neighbourhood, never a home address"
          },
          "date_lost": {
            "type": "string",
            "format": "date"
          },
          "status": {
            "enum": [
              "active",
              "found",
              "expired"
            ]
          },
          "public_contact": {
            "$ref": "#/components/schemas/PublicContact"
          },
          "type": {
            "enum": [
              "lost",
              "found"
            ],
            "default": "lost",
            "description": "t212: two-way board"
          },
          "found_location": {
            "$ref": "#/components/schemas/FoundLocation"
          },
          "date_found": {
            "type": "string",
            "format": "date"
          },
          "contact_public": {
            "type": "boolean",
            "default": false
          },
          "consented_at": {
            "type": "string",
            "format": "date-time",
            "description": "Server-set timestamp of consent; returned read-only"
          }
        },
        "additionalProperties": false,
        "if": {
          "properties": {
            "contact_public": {
              "const": false
            }
          }
        },
        "then": {
          "not": {
            "required": [
              "public_contact"
            ]
          }
        }
      },
      "LostPetCreate": {
        "type": "object",
        "required": [
          "species",
          "description",
          "city",
          "owner_name",
          "owner_email"
        ],
        "properties": {
          "pet_name": {
            "type": "string"
          },
          "species": {
            "type": "string"
          },
          "breed_colour": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "last_seen_area": {
            "type": "string"
          },
          "date_lost": {
            "type": "string",
            "format": "date"
          },
          "photos": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "microchipped": {
            "enum": [
              "yes",
              "no",
              "unknown"
            ]
          },
          "microchip_number": {
            "type": "string",
            "description": "Private, never returned"
          },
          "owner_name": {
            "type": "string"
          },
          "owner_email": {
            "type": "string",
            "format": "email"
          },
          "owner_phone": {
            "type": "string"
          },
          "public_contact_fields": {
            "type": "array",
            "items": {
              "enum": [
                "phone",
                "email"
              ]
            },
            "description": "Which details to show; only used when contact_public=true"
          },
          "submitted_by_agent": {
            "type": "boolean",
            "default": false
          },
          "type": {
            "enum": [
              "lost",
              "found"
            ],
            "default": "lost"
          },
          "found_location": {
            "$ref": "#/components/schemas/FoundLocation"
          },
          "date_found": {
            "type": "string",
            "format": "date"
          },
          "contact_public": {
            "type": "boolean",
            "default": false,
            "description": "t215 single opt-in flag. Default false: contact stays private and is relayed through MapXPet. True only if the user checked: 'Show my contact details publicly on this listing. I understand that once I share my contact details, MapXPet is not responsible for how others use them.'"
          },
          "contact_consent_text_version": {
            "type": "string",
            "description": "Version id of the consent text shown (e.g. 2026-11-01). Required when contact_public=true."
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "type": {
                  "const": "found"
                }
              },
              "required": [
                "type"
              ]
            },
            "then": {
              "required": [
                "photos",
                "found_location"
              ],
              "properties": {
                "photos": {
                  "minItems": 1
                }
              }
            },
            "else": {
              "required": [
                "last_seen_area",
                "date_lost"
              ]
            }
          },
          {
            "if": {
              "properties": {
                "contact_public": {
                  "const": true
                }
              },
              "required": [
                "contact_public"
              ]
            },
            "then": {
              "required": [
                "contact_consent_text_version"
              ]
            }
          }
        ],
        "description": "t212: type=found requires photos (at least one real, current photo of the animal) and found_location; missing either returns 422 missing_required. Found posts must not include microchip numbers or unique markings. No selling or rehoming found pets."
      },
      "BreedingPublic": {
        "type": "object",
        "description": "public_contact (phone/email) is returned only when contact_public=true.",
        "properties": {
          "id": {
            "type": "string"
          },
          "pet_name": {
            "type": "string"
          },
          "species": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "photos": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "breed": {
            "type": "string"
          },
          "age": {
            "type": "string"
          },
          "sex": {
            "enum": [
              "female",
              "male"
            ]
          },
          "registration": {
            "type": "string"
          },
          "health_notes": {
            "type": "string"
          },
          "public_contact": {
            "$ref": "#/components/schemas/PublicContact"
          },
          "contact_public": {
            "type": "boolean",
            "default": false
          },
          "consented_at": {
            "type": "string",
            "format": "date-time",
            "description": "Server-set timestamp of consent; returned read-only"
          }
        },
        "additionalProperties": false,
        "if": {
          "properties": {
            "contact_public": {
              "const": false
            }
          }
        },
        "then": {
          "not": {
            "required": [
              "public_contact"
            ]
          }
        }
      },
      "BreedingCreate": {
        "type": "object",
        "required": [
          "species",
          "breed",
          "sex",
          "city",
          "description"
        ],
        "properties": {
          "pet_name": {
            "type": "string"
          },
          "species": {
            "type": "string"
          },
          "breed": {
            "type": "string"
          },
          "age": {
            "type": "string"
          },
          "sex": {
            "enum": [
              "female",
              "male"
            ]
          },
          "registration": {
            "type": "string"
          },
          "health_notes": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "photos": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "contact_public": {
            "type": "boolean",
            "default": false,
            "description": "t215 single opt-in flag. Default false: contact stays private and is relayed through MapXPet. True only if the user checked: 'Show my contact details publicly on this listing. I understand that once I share my contact details, MapXPet is not responsible for how others use them.'"
          },
          "public_contact_fields": {
            "type": "array",
            "items": {
              "enum": [
                "phone",
                "email"
              ]
            },
            "description": "Which details to show; only used when contact_public=true"
          },
          "contact_consent_text_version": {
            "type": "string",
            "description": "Version id of the consent text shown (e.g. 2026-11-01). Required when contact_public=true."
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "contact_public": {
                  "const": true
                }
              },
              "required": [
                "contact_public"
              ]
            },
            "then": {
              "required": [
                "contact_consent_text_version"
              ]
            }
          }
        ]
      },
      "ListingStatus": {
        "type": "string",
        "enum": [
          "pending_scan",
          "held_for_review",
          "active",
          "rejected",
          "appeal_pending",
          "rejected_final",
          "hidden_pending_review",
          "removed"
        ],
        "default": "active",
        "description": "Auto-set to hidden_pending_review after report_threshold (3) reports from distinct members. Scan: pending_scan -> active (approved) or rejected; appeal: rejected -> appeal_pending -> active (approved) or rejected_final. One appeal per listing. Image flag (nudity_sexual or not_matching_pet) -> held_for_review until an admin approves (active) or removes (removed); applies to members too."
      },
      "SalePublic": {
        "type": "object",
        "description": "Public subset: no seller contact, health records or registration.",
        "properties": {
          "id": {
            "type": "string"
          },
          "pet_name": {
            "type": "string"
          },
          "species": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "photos": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "breed": {
            "type": "string"
          },
          "age": {
            "type": "string"
          },
          "price_cad": {
            "type": [
              "number",
              "null"
            ]
          },
          "status": {
            "$ref": "#/components/schemas/ListingStatus"
          }
        },
        "additionalProperties": false
      },
      "SaleFull": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SalePublic"
          }
        ],
        "type": "object",
        "properties": {
          "health_records": {
            "type": "string"
          },
          "registration": {
            "type": "string"
          },
          "seller_contact": {
            "$ref": "#/components/schemas/PublicContact"
          },
          "report_threshold": {
            "const": 3
          },
          "contact_public": {
            "type": "boolean",
            "default": false
          },
          "consented_at": {
            "type": "string",
            "format": "date-time",
            "description": "Server-set timestamp of consent; returned read-only"
          }
        },
        "description": " seller_contact is returned (to members) only when contact_public=true; otherwise members message the seller through MapXPet.",
        "if": {
          "properties": {
            "contact_public": {
              "const": false
            }
          }
        },
        "then": {
          "not": {
            "required": [
              "seller_contact"
            ]
          }
        }
      },
      "SaleCreate": {
        "type": "object",
        "required": [
          "species",
          "description",
          "city"
        ],
        "properties": {
          "pet_name": {
            "type": "string"
          },
          "species": {
            "type": "string"
          },
          "breed": {
            "type": "string"
          },
          "age": {
            "type": "string"
          },
          "price_cad": {
            "type": [
              "number",
              "null"
            ]
          },
          "description": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "health_records": {
            "type": "string"
          },
          "registration": {
            "type": "string"
          },
          "seller_phone": {
            "type": "string"
          },
          "seller_email": {
            "type": "string",
            "format": "email"
          },
          "photos": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "contact_public": {
            "type": "boolean",
            "default": false,
            "description": "t215 single opt-in flag. Default false: contact stays private and is relayed through MapXPet. True only if the user checked: 'Show my contact details to members on this listing. I understand that once I share my contact details, MapXPet is not responsible for how others use them.' For sale listings, shown to signed-in members only; otherwise members message through MapXPet."
          },
          "public_contact_fields": {
            "type": "array",
            "items": {
              "enum": [
                "phone",
                "email"
              ]
            },
            "description": "Which details to show; only used when contact_public=true"
          },
          "contact_consent_text_version": {
            "type": "string",
            "description": "Version id of the consent text shown (e.g. 2026-11-01). Required when contact_public=true."
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "contact_public": {
                  "const": true
                }
              },
              "required": [
                "contact_public"
              ]
            },
            "then": {
              "required": [
                "contact_consent_text_version"
              ]
            }
          }
        ]
      },
      "NotMemberWithPublic": {
        "type": "object",
        "properties": {
          "error": {
            "const": "not_member"
          },
          "join_url": {
            "const": "https://mapxpet.com/membership/"
          },
          "listing": {
            "$ref": "#/components/schemas/SalePublic"
          }
        }
      },
      "MessageCreate": {
        "type": "object",
        "required": [
          "body"
        ],
        "properties": {
          "sender_name": {
            "type": "string"
          },
          "sender_email": {
            "type": "string",
            "format": "email",
            "description": "Lost board only (anonymous); members use their account"
          },
          "body": {
            "type": "string",
            "maxLength": 2000
          },
          "turnstile_token": {
            "type": "string"
          }
        }
      },
      "MessageAccepted": {
        "type": "object",
        "description": "Never includes owner contact.",
        "properties": {
          "message_id": {
            "type": "string"
          },
          "status": {
            "const": "relayed"
          }
        },
        "additionalProperties": false
      },
      "ReportCreate": {
        "type": "object",
        "required": [
          "reason"
        ],
        "properties": {
          "reason": {
            "enum": [
              "fake",
              "scam",
              "misleading",
              "welfare",
              "other"
            ]
          },
          "details": {
            "type": "string",
            "maxLength": 1000
          }
        }
      },
      "ScanResult": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "approved",
              "rejected",
              "held_for_review"
            ]
          },
          "reason_category": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "account",
              "photo_reuse",
              "price",
              "mismatch",
              "other",
              null
            ],
            "description": "Coarse reason when rejected; detection details are never returned."
          },
          "appeal_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Present for rejected member listings; 'Ask for a second look'."
          },
          "image_flag": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "nudity_sexual",
              "not_matching_pet",
              null
            ]
          }
        }
      },
      "AppealCreate": {
        "type": "object",
        "properties": {
          "note": {
            "type": "string",
            "maxLength": 1000
          }
        }
      },
      "Appeal": {
        "type": "object",
        "properties": {
          "appeal_id": {
            "type": "string"
          },
          "listing_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "appeal_pending",
              "approved",
              "rejected_final"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PhotoUploadResult": {
        "type": "object",
        "properties": {
          "photo_id": {
            "type": "string"
          },
          "moderation": {
            "type": "string",
            "enum": [
              "pending",
              "ok",
              "flagged"
            ]
          },
          "flag_category": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "nudity_sexual",
              "not_matching_pet",
              null
            ]
          }
        },
        "description": "Image check result for one uploaded photo. A flagged photo holds the whole listing for admin review."
      },
      "FoundLocation": {
        "type": "object",
        "description": "Where the animal was found: city + nearest intersection or landmark. Never a home address.",
        "required": [
          "city",
          "area"
        ],
        "properties": {
          "city": {
            "type": "string",
            "minLength": 1
          },
          "area": {
            "type": "string",
            "minLength": 1,
            "description": "Nearest intersection or landmark"
          }
        },
        "additionalProperties": false
      },
      "InterestCreate": {
        "type": "object",
        "properties": {
          "note": {
            "type": "string",
            "maxLength": 280
          },
          "human": {
            "type": "object",
            "properties": {
              "email": {
                "type": "string",
                "format": "email"
              }
            },
            "description": "Required for lost-found without a bearer token; kept private"
          },
          "callback_url": {
            "type": "string",
            "format": "uri",
            "description": "Optional https webhook; receives status changes (HMAC-signed)"
          }
        },
        "additionalProperties": false
      },
      "InterestStatus": {
        "type": "object",
        "properties": {
          "interest_id": {
            "type": "string"
          },
          "status": {
            "enum": [
              "pending_confirmation",
              "pinged",
              "shared",
              "declined",
              "expired"
            ]
          },
          "status_url": {
            "type": "string",
            "format": "uri"
          },
          "status_token": {
            "type": "string",
            "description": "Only for email-only lost-found pings; shown once"
          },
          "owner_contact": {
            "$ref": "#/components/schemas/PublicContact",
            "description": "Present only when status=shared; private to this pinger"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "interest_id",
          "status"
        ]
      },
      "InterestOwnerRow": {
        "type": "object",
        "description": "Anonymous: never includes pinger name, email or member id",
        "properties": {
          "id": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "note": {
            "type": "string"
          },
          "via_ai_assistant": {
            "type": "boolean",
            "description": "Deprecated alias of source == ai_assistant",
            "deprecated": true
          },
          "status": {
            "enum": [
              "pinged",
              "shared",
              "declined",
              "expired"
            ]
          },
          "source": {
            "type": "string",
            "enum": [
              "human",
              "ai_assistant"
            ],
            "description": "Decided t224: ai_assistant pings show a neutral \"via AI assistant\" tag in the owner's anonymous list; never reveals which bot or person."
          }
        },
        "additionalProperties": false
      },
      "InterestShare": {
        "type": "object",
        "required": [
          "fields",
          "contact_consent_text_version"
        ],
        "properties": {
          "fields": {
            "type": "array",
            "minItems": 1,
            "items": {
              "enum": [
                "phone",
                "email"
              ]
            }
          },
          "contact_consent_text_version": {
            "type": "string"
          }
        }
      },
      "DraftCreate": {
        "type": "object",
        "description": "Same fields as the board's create schema (LostPetCreate / BreedingCreate / SaleCreate) plus human.email (lost-found) and callback_url. contact_public is only a suggestion; the human confirms it and the disclaimer on the review page.",
        "properties": {
          "listing": {
            "type": "object",
            "description": "Board create fields; found pets require photos (min 1) and found_location"
          },
          "human": {
            "type": "object",
            "properties": {
              "email": {
                "type": "string",
                "format": "email"
              }
            }
          },
          "callback_url": {
            "type": "string",
            "format": "uri"
          }
        },
        "additionalProperties": true
      },
      "DraftStatus": {
        "type": "object",
        "required": [
          "draft_id",
          "status"
        ],
        "properties": {
          "draft_id": {
            "type": "string"
          },
          "status": {
            "enum": [
              "draft_pending_owner",
              "pending_scan",
              "active",
              "held",
              "rejected",
              "discarded",
              "expired"
            ]
          },
          "review_sent_to": {
            "type": "string",
            "description": "Masked email"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "7 days after creation"
          },
          "status_url": {
            "type": "string",
            "format": "uri"
          },
          "listing_id": {
            "type": "string",
            "description": "Set after approval"
          },
          "scan": {
            "$ref": "#/components/schemas/ScanResult"
          }
        }
      }
    },
    "securitySchemes": {
      "memberBearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "Member token from the membership API; checked for entitlements breeding_board / pets_for_sale"
      },
      "manageToken": {
        "type": "apiKey",
        "in": "query",
        "name": "token",
        "description": "Single-listing manage token emailed to lost-pet owner"
      },
      "interestStatusToken": {
        "type": "apiKey",
        "in": "query",
        "name": "status_token",
        "description": "Returned for email-only lost-found pings; reads that one interest's status"
      },
      "signedReviewLink": {
        "type": "apiKey",
        "in": "query",
        "name": "sig",
        "description": "Single-use signed token from the review email; human only"
      },
      "appealLinkToken": {
        "type": "apiKey",
        "in": "query",
        "name": "t",
        "description": "Signed, single-listing appeal token sent to non-members in the decision email / email-confirmed flow (t223). Valid only for POST .../appeal on that listing."
      }
    }
  },
  "x-internal-admin": {
    "description": "Internal only, behind Cloudflare Access; not part of the public API.",
    "endpoints": {
      "POST /internal/moderation/appeals/{appeal_id}/resolve": {
        "body": {
          "decision": "approved|rejected_final",
          "note": "string (optional)"
        },
        "effect": "Admin decision is final. approved -> listing active; rejected_final -> listing stays hidden."
      },
      "POST /internal/moderation/listings/{id}/restore": {},
      "POST /internal/moderation/listings/{id}/remove": {},
      "POST /internal/moderation/held/{listing_id}/resolve": {
        "body": {
          "decision": "approve|remove"
        },
        "effect": "Resolves a held_for_review listing after an image flag."
      }
    }
  },
  "webhooks": {
    "scam_mention_alert": {
      "post": {
        "summary": "Admin alert on the FIRST scam mention on any listing (no 3-report wait)",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "event": {
                    "const": "scam_mention_alert"
                  },
                  "listing_type": {
                    "type": "string"
                  },
                  "listing_id": {
                    "type": "string"
                  },
                  "source": {
                    "type": "string",
                    "enum": [
                      "report_reason",
                      "report_details_keyword",
                      "message_keyword"
                    ]
                  },
                  "report_id": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "created_at": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "received"
          }
        }
      }
    },
    "image_flag_alert": {
      "post": {
        "summary": "Immediate admin alert when a photo is flagged; listing held_for_review",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "event": {
                    "const": "image_flag_alert"
                  },
                  "listing_type": {
                    "type": "string"
                  },
                  "listing_id": {
                    "type": "string"
                  },
                  "photo_id": {
                    "type": "string"
                  },
                  "flag_category": {
                    "type": "string",
                    "enum": [
                      "nudity_sexual",
                      "not_matching_pet"
                    ]
                  },
                  "created_at": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "received"
          }
        }
      }
    }
  },
  "x-webhooks": {
    "interest.status_changed": {
      "description": "POSTed to callback_url when status changes; body InterestStatus; header MapXPet-Signature (HMAC-SHA256)."
    },
    "draft.status_changed": {
      "description": "POSTed to callback_url; body DraftStatus; MapXPet-Signature header"
    }
  },
  "externalDocs": {
    "description": "MapXPet API for AI agents (human-readable guide; API not live yet)",
    "url": "https://mapxpet.com/for-agents/"
  }
}