{
  "openapi": "3.1.0",
  "info": {
    "title": "WYDM Agent API",
    "version": "1.0.0",
    "description": "REST API for autonomous agents operating a WYDM (Would You Date Me) account on their owner's behalf.\n\nEvery request is authenticated with a bearer agent key (`wydm_agent_…`) that resolves to exactly ONE owner user. An agent is authorized like that human — no elevated caps, no agent-only powers. Each route requires a specific scope granted to the key (see `x-required-scope` on each operation); a key missing the scope gets `403 INSUFFICIENT_SCOPE`.\n\nAll success responses are wrapped in a `{ \"data\": … }` envelope (`application/json`). All error responses are RFC 9457 problem documents (`application/problem+json`) with a stable machine-readable `code`. Field names on the wire are snake_case.\n\nEvery response — success and error — carries an `X-Request-Id` header (a stable per-request id). Quote it in a support request. An error body repeats it as the `instance` URN (`urn:wydm:request:<id>`). A `429 RATE_LIMITED` response also carries a `Retry-After` header in whole seconds; the per-route limits are fixed and published in the docs.\n\nThe owner can pause all agent access at any time (kill switch) — while paused, every route returns `403 AGENT_PAUSED`.\n\nThe agent platform requires an active **Ultra** subscription (minted in the WYDM iOS/Android app). Every route checks the owner's live tier: a lapsed subscription returns `403 ULTRA_REQUIRED` on every call until the owner resubscribes. Minting keys (and OAuth consent grants) is gated the same way.\n\n## Webhooks\n\nBeyond the request/response routes above, WYDM can PUSH events to an endpoint you register (see the `/agent/v1/webhooks` routes). The top-level `webhooks` section of this document describes every subscribable event and its payload.\n\n**Delivery.** WYDM sends an HTTP `POST` with a signed JSON body to your registered URL. The body is a snake_case envelope: `{ \"id\": \"<delivery id>\", \"event\": \"<event>\", \"occurred_at\": <ms epoch>, \"data\": { \"title\": \"…\", \"body\": \"…\", \"link\": \"…\"|null, \"dedupe_key\": \"…\", …event-specific fields } }`.\n\n**Headers.** `Content-Type: application/json`, `X-Webhook-Event: <event>`, `X-Webhook-Timestamp: <unix seconds>`, and `X-Signature: <lowercase hex HMAC-SHA256 of `timestamp + \".\" + rawBody`, keyed with the endpoint's signing secret>`. Your receiver MUST recompute `HMAC-SHA256(secret, X-Webhook-Timestamp + \".\" + rawBody)` over the raw body bytes (do not re-serialize a parsed copy — key ordering must match) and constant-time compare it to `X-Signature` before trusting the payload. Reject deliveries whose `X-Webhook-Timestamp` is too far from your clock to blunt replay attacks. (Note: `X-Webhook-Timestamp` is the delivery-attempt time and is fresh on each retry — it is distinct from the envelope's `occurred_at`, which is the stable event time.)\n\n**Reliability.** Delivery is at-least-once with exponential-backoff retry (up to 10 attempts; ~17 minute delivery window). Each attempt is a fresh request with its own `id`, so DEDUPE on `data.dedupe_key`, not on `id`. A 2xx response acks the delivery; any non-2xx (or a network error) is retried. A URL that resolves to a blocked/private address (SSRF guard) is dropped permanently with no retry. An endpoint that fails ~20 deliveries in a row is auto-disabled (its `disabled_at`/`disabled_reason` are then set on the list response) and the owner is notified in-app; re-register the endpoint to re-enable delivery. Delivery logs are retained ~30 days.\n\n**Approval gate.** An agent-registered endpoint is INERT until the owner approves it out-of-band by email (Phase 3c) — no events are delivered until then. Subscribing to an event requires the `webhooks:write` scope to register the endpoint PLUS the per-event read scope listed on each event below (`x-required-scopes`).",
    "contact": {
      "name": "WYDM",
      "url": "https://wydm.app"
    }
  },
  "servers": [
    {
      "url": "https://api.wydm.app/agent/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "agentKey": []
    }
  ],
  "tags": [
    {
      "name": "Identity & profile",
      "description": "Read the owner identity, read/edit the profile, manage publish lifecycle."
    },
    {
      "name": "Discovery",
      "description": "Browse the feed, read the live map, tune discovery settings."
    },
    {
      "name": "Ratings",
      "description": "Submit and retract yes/no verdicts on other profiles."
    },
    {
      "name": "Media",
      "description": "Upload/reorder/delete photos and videos (presign then finalize). Uploads are NSFW/CSAM scanned."
    },
    {
      "name": "Direct messages",
      "description": "Send DMs and read conversation context; accept/deny inbound message requests."
    },
    {
      "name": "Safety",
      "description": "Block and report other users on the owner's behalf."
    },
    {
      "name": "Notifications",
      "description": "Read the owner's notification inbox."
    },
    {
      "name": "Webhooks",
      "description": "Register/list/delete webhook endpoints (owner-approved before delivery)."
    },
    {
      "name": "Approvals",
      "description": "Request out-of-band human approval for a sensitive action and poll the verdict."
    }
  ],
  "paths": {
    "/whoami": {
      "get": {
        "operationId": "whoami",
        "summary": "Resolve the token owner's identity",
        "description": "Returns the owner user the key resolves to, plus the scopes the key carries. Requires scope `account:read`.",
        "tags": [
          "Identity & profile"
        ],
        "x-required-scope": "account:read",
        "responses": {
          "200": {
            "description": "The owner identity.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "user_id",
                        "display_name",
                        "status",
                        "agent_assisted",
                        "scopes",
                        "key_expires_at"
                      ],
                      "properties": {
                        "user_id": {
                          "type": "string"
                        },
                        "display_name": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "status": {
                          "type": "string",
                          "description": "Account status, e.g. active."
                        },
                        "agent_assisted": {
                          "type": "boolean"
                        },
                        "scopes": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "key_expires_at": {
                          "type": [
                            "number",
                            "null"
                          ],
                          "description": "The calling key's expiry as epoch milliseconds, or null when the key never expires."
                        }
                      }
                    }
                  },
                  "example": {
                    "data": {
                      "user_id": "k17abc123",
                      "display_name": "Ada",
                      "status": "active",
                      "agent_assisted": true,
                      "scopes": [
                        "account:read",
                        "profile:write",
                        "discovery:read"
                      ],
                      "key_expires_at": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/profile": {
      "get": {
        "operationId": "get_profile",
        "summary": "Read the owner's full profile with media moderation state",
        "description": "Returns the owner's profile plus every media row INCLUDING moderation state (`scanning_nsfw`, `rejected_nsfw`, `csam_match`, etc.) so an agent can notice a rejected upload. Requires scope `account:read`.\n\nThrows `409 NEEDS_PROFILE` when the owner has no profile row yet.",
        "tags": [
          "Identity & profile"
        ],
        "x-required-scope": "account:read",
        "responses": {
          "200": {
            "description": "The owner profile, share-card URL, and merged media list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "profile",
                        "public_url",
                        "card_image_url",
                        "media"
                      ],
                      "properties": {
                        "profile": {
                          "$ref": "#/components/schemas/Profile"
                        },
                        "public_url": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "Public profile URL when a slug is allocated."
                        },
                        "card_image_url": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "Shareable share-card PNG URL; null unless published + public with a published photo."
                        },
                        "media": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/MediaItem"
                          }
                        }
                      }
                    }
                  },
                  "example": {
                    "data": {
                      "profile": {
                        "id": "prof_9f2",
                        "user_id": "k17abc123",
                        "display_name": "Ada",
                        "bio": "Systems, synths, and long hikes.",
                        "gender": "woman",
                        "looking_for": [
                          "men",
                          "women"
                        ],
                        "education": "Imperial College",
                        "education_level": "masters",
                        "job": "Engineer",
                        "hometown": "London",
                        "religion": null,
                        "politics": null,
                        "languages": [
                          "English",
                          "Italian"
                        ],
                        "height": 170,
                        "vices": [
                          "coffee"
                        ],
                        "dating_intentions": [
                          "long_term"
                        ],
                        "prompts": [
                          {
                            "prompt_id": "pr_1",
                            "category": "about",
                            "prompt_text": "A perfect Sunday",
                            "answer": "Espresso then a trail.",
                            "voice": null
                          }
                        ],
                        "theme_song": null,
                        "location": {
                          "latitude": 51.5072,
                          "longitude": -0.1276,
                          "city": "London",
                          "country": "United Kingdom",
                          "country_code": "GB",
                          "source": "geocode",
                          "updated_at": 1737763200000
                        },
                        "live_location": null,
                        "slug": "ada",
                        "visibility": "public",
                        "published": true,
                        "published_at": 1737763200000,
                        "public_url": "https://wydm.app/p/ada",
                        "created_at": 1737000000000,
                        "updated_at": 1737763200000
                      },
                      "public_url": "https://wydm.app/p/ada",
                      "card_image_url": "https://api.wydm.app/card/ada.png",
                      "media": [
                        {
                          "id": "med_1",
                          "position": 1,
                          "url": "https://cdn.wydm.app/p/prof_9f2/1.jpg",
                          "type": "photo",
                          "state": "published",
                          "rejected_reason": null
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "The owner has no profile yet. `code`: `NEEDS_PROFILE`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "update_profile",
        "summary": "Edit the owner's profile text fields",
        "description": "Patches only the keys provided (a missing key is left unchanged; an explicit `null` clears a nullable field). `display_name` is stored on the user row. Requires scope `profile:write`.\n\nEach `prompt_answers` entry uses `prompt_id`; an entry with no usable prompt id is a `400 INVALID_ARGUMENT`.\n\n`search_indexing` opts the profile in to (or out of) search-engine indexing at `/pub/<slug>`; `/p/<slug>` is never indexed.\n\nErrors: `400 INVALID_ARGUMENT` (wrong-typed field); `409 NOT_PUBLISHABLE` (`search_indexing: true` on an unpublished, private or not-ready profile); `429 RATE_LIMITED` (profile-write rate limit, 30/min).",
        "tags": [
          "Identity & profile"
        ],
        "x-required-scope": "profile:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProfilePatch"
              },
              "example": {
                "bio": "Systems, synths, and long hikes.",
                "job": "Staff Engineer",
                "looking_for": [
                  "men",
                  "women"
                ],
                "height": 170,
                "prompt_answers": [
                  {
                    "prompt_id": "pr_1",
                    "answer": "Espresso then a trail."
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Update acknowledged.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "updated"
                      ],
                      "properties": {
                        "updated": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "updated": true
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "Search indexing can't be turned on for this profile yet. `code`: `NOT_PUBLISHABLE`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/readiness": {
      "get": {
        "operationId": "get_readiness",
        "summary": "Read publish-readiness rules",
        "description": "Returns whether the owner can publish and the pass/fail of every gating rule (email_verified, dob_present, age_18_plus, has_photo, no_scanning, no_rejected). Requires scope `account:read`.",
        "tags": [
          "Identity & profile"
        ],
        "x-required-scope": "account:read",
        "responses": {
          "200": {
            "description": "Readiness state.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "can_publish",
                        "rules"
                      ],
                      "properties": {
                        "can_publish": {
                          "type": "boolean"
                        },
                        "rules": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/ReadinessRule"
                          }
                        }
                      }
                    }
                  },
                  "example": {
                    "data": {
                      "can_publish": false,
                      "rules": [
                        {
                          "id": "email_verified",
                          "label": "Email verified",
                          "passed": true
                        },
                        {
                          "id": "has_photo",
                          "label": "At least one published photo",
                          "passed": false
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/prompts": {
      "post": {
        "operationId": "set_prompts",
        "summary": "Set the owner's prompt answers",
        "description": "Sets up to 3 text prompt answers. Every `prompt_id` must be an ACTIVE prompt. Text answers only (voice memos need the app). Requires scope `profile:write`. The id field is `prompt_id`.\n\nErrors: `400 INVALID_PROMPTS` (malformed, >3, empty, or unknown/inactive prompt_id); `429 RATE_LIMITED`.",
        "tags": [
          "Identity & profile"
        ],
        "x-required-scope": "profile:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "prompt_answers"
                ],
                "properties": {
                  "prompt_answers": {
                    "type": "array",
                    "maxItems": 3,
                    "items": {
                      "type": "object",
                      "required": [
                        "prompt_id",
                        "answer"
                      ],
                      "properties": {
                        "prompt_id": {
                          "type": "string"
                        },
                        "answer": {
                          "type": "string",
                          "description": "Non-empty; capped at 150 chars downstream."
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "prompt_answers": [
                  {
                    "prompt_id": "pr_1",
                    "answer": "Espresso then a trail."
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Prompts saved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "updated",
                        "count"
                      ],
                      "properties": {
                        "updated": {
                          "type": "boolean"
                        },
                        "count": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "updated": true,
                    "count": 1
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid prompt answers. `code`: `INVALID_PROMPTS` or `INVALID_ARGUMENT`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/publish": {
      "post": {
        "operationId": "publish",
        "summary": "Publish the owner's profile",
        "description": "First-time slug allocation + flip to public, or idempotently returns the existing slug when already published. Body is ignored (send `{}`). Requires scope `profile:write`.\n\nErrors: `409 NOT_PUBLISHABLE` (readiness rules unmet — the unmet rule ids ride in `detail`); `429 RATE_LIMITED`.",
        "tags": [
          "Identity & profile"
        ],
        "x-required-scope": "profile:write",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {}
            }
          }
        },
        "responses": {
          "200": {
            "description": "Published (or already published).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "slug",
                        "public_url"
                      ],
                      "properties": {
                        "slug": {
                          "type": "string"
                        },
                        "public_url": {
                          "type": "string"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "slug": "ada",
                    "public_url": "https://wydm.app/p/ada"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "Profile not ready to publish. `code`: `NOT_PUBLISHABLE` (unmet rule ids in `detail`).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/feed": {
      "get": {
        "operationId": "get_feed",
        "summary": "Browse the discovery feed",
        "description": "Returns discovery cards. Point resolution: `lat`+`lng` used verbatim; else `city` is geocoded; else the owner's own live/anchor location is used. Requires scope `discovery:read`.\n\nErrors: `400 NEEDS_LOCATION` (no point and no stored location); `404 NOT_FOUND` (city could not be geocoded); `409 NEEDS_PROFILE`; `429 RATE_LIMITED` (geocoder).",
        "tags": [
          "Discovery"
        ],
        "x-required-scope": "discovery:read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Max cards to return."
          },
          {
            "name": "lat",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number"
            },
            "description": "Latitude; pair with lng."
          },
          {
            "name": "lng",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number"
            },
            "description": "Longitude; pair with lat."
          },
          {
            "name": "city",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "City name to geocode when lat/lng omitted."
          },
          {
            "name": "radius_km",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number"
            },
            "description": "Search radius in km."
          },
          {
            "name": "live",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            },
            "description": "Use the live-location index when 'true'."
          }
        ],
        "responses": {
          "200": {
            "description": "Feed cards.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "cards"
                      ],
                      "properties": {
                        "cards": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/FeedCard"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "cards": [
                      {
                        "slug": "sam",
                        "display_name": "Sam",
                        "first_name": "Sam",
                        "age": 29,
                        "lead_photo_url": "https://cdn.wydm.app/p/x/1.jpg",
                        "distance_km": 3.2
                      },
                      {
                        "slug": "kai",
                        "display_name": "Kai",
                        "first_name": "Kai",
                        "age": 31,
                        "lead_photo_url": null,
                        "distance_km": 7.8,
                        "previous_verdict": "yes"
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "No location to search from. `code`: `NEEDS_LOCATION`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "No published profile. `code`: `NEEDS_PROFILE`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/nearby_live": {
      "get": {
        "operationId": "get_nearby_live",
        "summary": "Read the live map (jittered markers)",
        "description": "Premium-only live map of nearby users with a fresh ping. Coordinates are the JITTERED privacy-offset point, never the target's raw location. Requires scope `discovery:read`.\n\nErrors: `403 NOT_PREMIUM`; `409 NEEDS_PROFILE`; `400 NEEDS_LOCATION`.",
        "tags": [
          "Discovery"
        ],
        "x-required-scope": "discovery:read",
        "responses": {
          "200": {
            "description": "Live markers.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "markers"
                      ],
                      "properties": {
                        "markers": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/LiveMarker"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "markers": [
                      {
                        "slug": "sam",
                        "first_name": "Sam",
                        "age": 29,
                        "lead_photo_url": "https://cdn.wydm.app/p/x/1.jpg",
                        "jittered_lat": 51.51,
                        "jittered_lng": -0.13,
                        "online": true,
                        "checked_in_minutes_ago": 2
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "No location to anchor the map. `code`: `NEEDS_LOCATION`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Insufficient scope, agent paused, Ultra lapsed, or premium required. `code`: `INSUFFICIENT_SCOPE`, `AGENT_PAUSED`, `ULTRA_REQUIRED`, or `NOT_PREMIUM`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "No published profile. `code`: `NEEDS_PROFILE`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/discovery_settings": {
      "patch": {
        "operationId": "set_discovery_settings",
        "summary": "Update discovery preferences",
        "description": "Upserts the owner's discovery radius (clamped to [1,100] km) and/or hide-from-discovery flag. Both fields optional; an empty body is a no-op that still returns `{ ok: true }`. Requires scope `profile:write`.\n\nErrors: `400 INVALID_ARGUMENT`.",
        "tags": [
          "Discovery"
        ],
        "x-required-scope": "profile:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "radius_km": {
                    "type": "number",
                    "description": "Clamped to [1,100]."
                  },
                  "hide_from_discovery": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "radius_km": 25,
                "hide_from_discovery": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Settings saved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "ok"
                      ],
                      "properties": {
                        "ok": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "ok": true
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/ratings": {
      "post": {
        "operationId": "submit_rating",
        "summary": "Submit a yes/no verdict on a profile",
        "description": "Rates a target addressed by public `slug`. The target must be visible to a rater (published, non-private, active owner). Subject to the SAME daily swipe cap (50/day free tier) a human hits. Requires scope `ratings:write`.\n\nErrors: `400 INVALID_ARGUMENT` (missing slug / verdict not yes|no); `404 NOT_FOUND` (no such visible profile); `429 RATE_LIMITED` (daily cap).\n\nThis operation is idempotent when you send an `Idempotency-Key` header: a retry with the same key and the same body replays the original response for 24 hours; the same key with a different body returns 409 `IDEMPOTENCY_CONFLICT`.",
        "tags": [
          "Ratings"
        ],
        "x-required-scope": "ratings:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "slug",
                  "verdict"
                ],
                "properties": {
                  "slug": {
                    "type": "string"
                  },
                  "verdict": {
                    "type": "string",
                    "enum": [
                      "yes",
                      "no"
                    ]
                  },
                  "note": {
                    "type": "string",
                    "description": "Optional private note attached to the rating."
                  }
                }
              },
              "example": {
                "slug": "sam",
                "verdict": "yes",
                "note": "Great taste in synths."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rating recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "rated"
                      ],
                      "properties": {
                        "rated": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "rated": true
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKeyHeader"
          }
        ]
      }
    },
    "/ratings/undo": {
      "post": {
        "operationId": "undo_rating",
        "summary": "Retract a verdict on a profile",
        "description": "Retracts the owner's public yes/no on a profile so its card returns to the deck. Premium (or an undo-swipe credit) is required. An unknown slug (or one the owner never rated) is an idempotent no-op returning `{ undone: false }`, not an error. Requires scope `ratings:write`.\n\nErrors: `403 NOT_PREMIUM`; `400 INVALID_ARGUMENT` (missing slug); `429 RATE_LIMITED`.",
        "tags": [
          "Ratings"
        ],
        "x-required-scope": "ratings:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "slug"
                ],
                "properties": {
                  "slug": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "slug": "sam"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Undo result (idempotent).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "undone"
                      ],
                      "properties": {
                        "undone": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "undone": true
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Insufficient scope, agent paused, Ultra lapsed, or premium required. `code`: `INSUFFICIENT_SCOPE`, `AGENT_PAUSED`, `ULTRA_REQUIRED`, or `NOT_PREMIUM`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/media/presign": {
      "post": {
        "operationId": "mediaPresign",
        "summary": "Presign a photo upload (step 1 of upload_photo)",
        "description": "Mints an R2 upload URL + storage key. The agent then PUTs the raw image bytes to `upload_url`, then calls `POST /media/finalize`. Requires scope `media:write`.\n\nFails closed with `503 MODERATION_UNAVAILABLE` if the NSFW/CSAM scan chain is not configured on the deployment.\n\nErrors: `400 INVALID_ARGUMENT` (bad content_type/position); `429 RATE_LIMITED`; `503 MODERATION_UNAVAILABLE`.",
        "tags": [
          "Media"
        ],
        "x-required-scope": "media:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "content_type"
                ],
                "properties": {
                  "content_type": {
                    "type": "string",
                    "enum": [
                      "image/jpeg",
                      "image/png",
                      "image/webp"
                    ]
                  },
                  "position": {
                    "type": "number",
                    "description": "Optional 1-based slot to place the photo."
                  }
                }
              },
              "example": {
                "content_type": "image/jpeg",
                "position": 2
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Upload URL and key. PUT the bytes to `upload_url`, then finalize.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "upload_url",
                        "key"
                      ],
                      "properties": {
                        "upload_url": {
                          "type": "string"
                        },
                        "key": {
                          "type": "string"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "upload_url": "https://r2.example.com/put/...signed",
                    "key": "profiles/prof_9f2/abc.jpg"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/ModerationUnavailable"
          }
        }
      }
    },
    "/media/finalize": {
      "post": {
        "operationId": "mediaFinalize",
        "summary": "Finalize a photo upload (step 2 of upload_photo)",
        "description": "Called after the R2 PUT succeeds. Records the photo (enters NSFW/CSAM scan). Requires scope `media:write`.\n\nFails closed with `503 MODERATION_UNAVAILABLE` if moderation is not configured.\n\nErrors: `403 KEY_FOREIGN` (key not owned); `409 MEDIA_LIMIT` (30-photo cap); `400 INVALID_ARGUMENT`; `503 MODERATION_UNAVAILABLE`.",
        "tags": [
          "Media"
        ],
        "x-required-scope": "media:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "key",
                  "content_type"
                ],
                "properties": {
                  "key": {
                    "type": "string",
                    "description": "The storage key returned by presign."
                  },
                  "content_type": {
                    "type": "string",
                    "enum": [
                      "image/jpeg",
                      "image/png",
                      "image/webp"
                    ]
                  },
                  "size_bytes": {
                    "type": "number"
                  },
                  "position": {
                    "type": "number"
                  }
                }
              },
              "example": {
                "key": "profiles/prof_9f2/abc.jpg",
                "content_type": "image/jpeg",
                "size_bytes": 284511,
                "position": 2
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created media row (state is typically `scanning_nsfw` until the scan clears).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "media"
                      ],
                      "properties": {
                        "media": {
                          "$ref": "#/components/schemas/MediaItem"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "media": {
                      "id": "med_9",
                      "position": 2,
                      "url": "https://cdn.wydm.app/p/prof_9f2/abc.jpg",
                      "type": "photo",
                      "state": "scanning_nsfw",
                      "rejected_reason": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Insufficient scope, agent paused, Ultra lapsed, or foreign key. `code`: `INSUFFICIENT_SCOPE`, `AGENT_PAUSED`, `ULTRA_REQUIRED`, or `KEY_FOREIGN`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Photo limit reached. `code`: `MEDIA_LIMIT`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/ModerationUnavailable"
          }
        }
      }
    },
    "/media/video/presign": {
      "post": {
        "operationId": "mediaVideoPresign",
        "summary": "Presign a video upload (step 1 of upload_video)",
        "description": "Mints upload URLs for the video (and an optional poster). The agent PUTs the MP4/MOV to `upload_url` (and poster to `poster_upload_url`), then calls `POST /media/video/finalize`. Limits: 6 videos, 90s, 500MB, video/mp4 or video/quicktime. Requires scope `media:write`.\n\nFails closed with `503 MODERATION_UNAVAILABLE` if the video scan channel is not configured.\n\nErrors: `400 INVALID_ARGUMENT`/`DURATION_TOO_LONG`; `413 SIZE_TOO_LARGE`; `415 UNSUPPORTED_TYPE`; `409 MAX_VIDEOS_EXCEEDED`; `429 RATE_LIMITED`; `503 MODERATION_UNAVAILABLE`.",
        "tags": [
          "Media"
        ],
        "x-required-scope": "media:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "size_bytes",
                  "duration_seconds",
                  "mime_type"
                ],
                "properties": {
                  "size_bytes": {
                    "type": "number",
                    "description": "Positive; ≤ 500MB."
                  },
                  "duration_seconds": {
                    "type": "number",
                    "description": "Positive; ≤ 90s."
                  },
                  "mime_type": {
                    "type": "string",
                    "enum": [
                      "video/mp4",
                      "video/quicktime"
                    ]
                  }
                }
              },
              "example": {
                "size_bytes": 18446720,
                "duration_seconds": 24,
                "mime_type": "video/mp4"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Video and poster upload URLs plus the new video id and storage keys.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "video_id",
                        "upload_url",
                        "storage_key",
                        "poster_upload_url",
                        "poster_storage_key"
                      ],
                      "properties": {
                        "video_id": {
                          "type": "string"
                        },
                        "upload_url": {
                          "type": "string"
                        },
                        "storage_key": {
                          "type": "string"
                        },
                        "poster_upload_url": {
                          "type": "string"
                        },
                        "poster_storage_key": {
                          "type": "string"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "video_id": "vid_3",
                    "upload_url": "https://r2.example.com/put/vid...signed",
                    "storage_key": "videos/prof_9f2/vid_3.mp4",
                    "poster_upload_url": "https://r2.example.com/put/poster...signed",
                    "poster_storage_key": "videos/prof_9f2/vid_3.jpg"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid argument or duration too long. `code`: `INVALID_ARGUMENT` or `DURATION_TOO_LONG`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "Video limit reached. `code`: `MAX_VIDEOS_EXCEEDED`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "Video too large. `code`: `SIZE_TOO_LARGE`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported video type. `code`: `UNSUPPORTED_TYPE`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/ModerationUnavailable"
          }
        }
      }
    },
    "/media/video/finalize": {
      "post": {
        "operationId": "mediaVideoFinalize",
        "summary": "Finalize a video upload (step 2 of upload_video)",
        "description": "Called after the video (and optional poster) PUT(s) succeed. Performs the one idempotent `uploading → scanning_nsfw` transition; the clip is NEVER set to `ready` here — only the async NSFW scan verdict can do that. Requires scope `media:write`.\n\nFails closed with `503 MODERATION_UNAVAILABLE` if video moderation is not configured.\n\nErrors: `400 INVALID_ARGUMENT`; `404 NOT_FOUND` (foreign/missing/malformed video_id); `503 MODERATION_UNAVAILABLE`.",
        "tags": [
          "Media"
        ],
        "x-required-scope": "media:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "video_id"
                ],
                "properties": {
                  "video_id": {
                    "type": "string"
                  },
                  "poster_uploaded": {
                    "type": "boolean",
                    "description": "Whether the poster image was also uploaded."
                  }
                }
              },
              "example": {
                "video_id": "vid_3",
                "poster_uploaded": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The clip's new state (async NSFW scan pending).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "state"
                      ],
                      "properties": {
                        "state": {
                          "type": "string",
                          "description": "e.g. scanning_nsfw."
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "state": "scanning_nsfw"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "503": {
            "$ref": "#/components/responses/ModerationUnavailable"
          }
        }
      }
    },
    "/media/order": {
      "patch": {
        "operationId": "reorder_media",
        "summary": "Reorder the owner's photos",
        "description": "Rewrites photo positions to the given order; the first id becomes the new lead photo. Returns the photos re-read in the new order. Requires scope `media:write`.\n\nErrors: `403 KEY_FOREIGN` (a media id not owned); `404 NOT_FOUND` (a media id does not exist); `400 INVALID_ARGUMENT` (media_ids not an array); `429 RATE_LIMITED`.",
        "tags": [
          "Media"
        ],
        "x-required-scope": "media:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "media_ids"
                ],
                "properties": {
                  "media_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Owner's photo ids in the desired order; index 0 = new lead."
                  }
                }
              },
              "example": {
                "media_ids": [
                  "med_9",
                  "med_1",
                  "med_4"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The reordered photos.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "media"
                      ],
                      "properties": {
                        "media": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/MediaItem"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "media": [
                      {
                        "id": "med_9",
                        "position": 1,
                        "url": "https://cdn.wydm.app/p/prof_9f2/abc.jpg",
                        "type": "photo",
                        "state": "published",
                        "rejected_reason": null
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Insufficient scope, agent paused, Ultra lapsed, or a foreign media id. `code`: `INSUFFICIENT_SCOPE`, `AGENT_PAUSED`, `ULTRA_REQUIRED`, or `KEY_FOREIGN`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/media/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "The media (photo) id."
        }
      ],
      "delete": {
        "operationId": "delete_media",
        "summary": "Delete one of the owner's photos",
        "description": "Removes a photo by id. A foreign OR malformed id both map to `404 NOT_FOUND` (never 403) so the route can't probe existence. Requires scope `media:write`.",
        "tags": [
          "Media"
        ],
        "x-required-scope": "media:write",
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "deleted"
                      ],
                      "properties": {
                        "deleted": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "deleted": true
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/dms": {
      "post": {
        "operationId": "send_dm",
        "summary": "Send a DM on the owner's behalf",
        "description": "Sends a message. Provide EXACTLY ONE of `recipient_slug` (open/append a conversation) or `thread_id` (reply into an existing thread). The body is sent verbatim; disclosure is the `sent_via` flag on the message, never injected text. Requires scope `dm:write`.\n\nErrors: `400 INVALID_ARGUMENT`/`EMPTY_BODY`/`BODY_TOO_LONG`/`SELF`; `403 BLOCKED`/`NOT_PREMIUM`; `404 RECIPIENT_NOT_FOUND`/`NOT_PARTICIPANT`; `409 NOT_RATED`/`REQUEST_PENDING`; `410 RECIPIENT_GONE`; `429 RATE_LIMITED`/`DM_THREAD_LIMIT_REACHED`.\n\nThis operation is idempotent when you send an `Idempotency-Key` header: a retry with the same key and the same body replays the original response for 24 hours; the same key with a different body returns 409 `IDEMPOTENCY_CONFLICT`.",
        "tags": [
          "Direct messages"
        ],
        "x-required-scope": "dm:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string",
                    "description": "Non-empty message text."
                  },
                  "recipient_slug": {
                    "type": "string",
                    "description": "Open/append a conversation with this profile. Mutually exclusive with thread_id."
                  },
                  "thread_id": {
                    "type": "string",
                    "description": "Reply into this existing thread. Mutually exclusive with recipient_slug."
                  }
                }
              },
              "example": {
                "recipient_slug": "sam",
                "body": "Hey! Loved your synth setup."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Message sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "thread_id",
                        "message_id",
                        "request_state",
                        "sent_via"
                      ],
                      "properties": {
                        "thread_id": {
                          "type": "string"
                        },
                        "message_id": {
                          "type": "string"
                        },
                        "request_state": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "accepted"
                          ],
                          "description": "Whether the thread is a still-pending message request."
                        },
                        "sent_via": {
                          "type": "string",
                          "enum": [
                            "agent"
                          ],
                          "description": "Always 'agent' for this route (drives the disclosure badge)."
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "thread_id": "thr_7",
                    "message_id": "msg_42",
                    "request_state": "pending",
                    "sent_via": "agent"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad argument. `code`: `INVALID_ARGUMENT`, `EMPTY_BODY`, `BODY_TOO_LONG`, or `SELF`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Insufficient scope, agent paused, Ultra lapsed, blocked, or premium required. `code`: `INSUFFICIENT_SCOPE`, `AGENT_PAUSED`, `ULTRA_REQUIRED`, `BLOCKED`, or `NOT_PREMIUM`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Recipient or thread not found. `code`: `RECIPIENT_NOT_FOUND` or `NOT_PARTICIPANT`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Must rate the recipient first, or a request is already pending. `code`: `NOT_RATED` or `REQUEST_PENDING`. Also `IDEMPOTENCY_CONFLICT` when an Idempotency-Key is reused with a different body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "410": {
            "description": "Recipient is no longer available. `code`: `RECIPIENT_GONE`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited or per-thread message limit reached. `code`: `RATE_LIMITED` or `DM_THREAD_LIMIT_REACHED`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKeyHeader"
          }
        ]
      },
      "get": {
        "operationId": "list_dms",
        "summary": "List the owner's DM threads",
        "description": "Lists accepted inbox threads (pending inbound requests live under `GET /dms/requests`). Read-only; no private media. Requires scope `dm:read`.\n\nCursor-paginated: pass the previous response's `next_cursor` back as `?cursor` to read older threads. Stop when `next_cursor` is `null`. Page size defaults to 25 (max 50). The cursor is opaque; a malformed value returns `400 INVALID_ARGUMENT`.",
        "tags": [
          "Direct messages"
        ],
        "x-required-scope": "dm:read",
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque pagination cursor. Pass back the previous response's `next_cursor` verbatim to read the next page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size. Default 25, max 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Thread summaries.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "threads",
                        "next_cursor"
                      ],
                      "properties": {
                        "threads": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/DmThreadSummary"
                          }
                        },
                        "next_cursor": {
                          "type": [
                            "number",
                            "null"
                          ],
                          "description": "Opaque cursor for the next (older) page, or null when there are no more threads."
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "threads": [
                      {
                        "thread_id": "thr_7",
                        "other": {
                          "slug": "sam",
                          "display_name": "Sam"
                        },
                        "last_message_at": 1737763200000,
                        "request_state": "accepted",
                        "is_initiator": true,
                        "unread_count": 0
                      }
                    ],
                    "next_cursor": 1737763100000
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/dms/requests": {
      "get": {
        "operationId": "list_requests",
        "summary": "List pending inbound message requests",
        "description": "Lists threads still `request_state === \"pending\"` where the owner is the recipient (non-initiator). Requires scope `dm:read`.",
        "tags": [
          "Direct messages"
        ],
        "x-required-scope": "dm:read",
        "responses": {
          "200": {
            "description": "Pending request threads.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "threads"
                      ],
                      "properties": {
                        "threads": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/DmRequestThread"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "threads": [
                      {
                        "id": "thr_9",
                        "other_user": {
                          "user_id": "k99",
                          "display_name": "Jo",
                          "avatar_url": "https://cdn.wydm.app/p/y/1.jpg",
                          "slug": "jo"
                        },
                        "last_message": {
                          "id": "msg_1",
                          "sender_id": "k99",
                          "body": "Hi there",
                          "read_at": null,
                          "created_at": 1737763200000,
                          "deleted_at": null,
                          "expired_at": null,
                          "attachment_kind": null,
                          "attachment_duration_ms": null
                        },
                        "unread_count": 1,
                        "initiator_user_id": "k99",
                        "blocked_by_user_id": null,
                        "request_state": "pending",
                        "created_at": 1737763100000,
                        "last_message_at": 1737763200000,
                        "pinned_at": null,
                        "muted_at": null,
                        "other_typing_expires_at": null
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/dms/requests/accept": {
      "post": {
        "operationId": "accept_request",
        "summary": "Accept a pending inbound message request",
        "description": "Flips a pending inbound request to accepted and notifies the initiator. Requires scope `dm:write`.\n\nErrors: `400 INVALID_ARGUMENT` (missing thread_id); `404 NOT_FOUND` (unknown thread, non-participant, or malformed id all collapse here); `409 NOT_RECIPIENT` (owner's own request); `429 RATE_LIMITED`.",
        "tags": [
          "Direct messages"
        ],
        "x-required-scope": "dm:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "thread_id"
                ],
                "properties": {
                  "thread_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "thread_id": "thr_9"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted (idempotent).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "accepted",
                        "already_accepted"
                      ],
                      "properties": {
                        "accepted": {
                          "type": "boolean"
                        },
                        "already_accepted": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "accepted": true,
                    "already_accepted": false
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Cannot accept your own request. `code`: `NOT_RECIPIENT`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/dms/requests/deny": {
      "post": {
        "operationId": "deny_request",
        "summary": "Deny a pending inbound message request",
        "description": "Denies (hard-deletes) a pending inbound request. Requires scope `dm:write`.\n\nErrors: `400 INVALID_ARGUMENT`; `404 NOT_FOUND` (unknown/non-participant/malformed collapse here); `409 NOT_RECIPIENT` (own request) or `NOT_PENDING` (already accepted/legacy thread); `429 RATE_LIMITED`.",
        "tags": [
          "Direct messages"
        ],
        "x-required-scope": "dm:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "thread_id"
                ],
                "properties": {
                  "thread_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "thread_id": "thr_9"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Denied.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "denied"
                      ],
                      "properties": {
                        "denied": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "denied": true
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Cannot deny your own request, or the request is no longer pending. `code`: `NOT_RECIPIENT` or `NOT_PENDING`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/dms/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "The DM thread id."
        },
        {
          "name": "cursor",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string"
          },
          "description": "Opaque pagination cursor. Pass back the previous response's `next_cursor` verbatim to read the next (older) page."
        }
      ],
      "get": {
        "operationId": "read_dm_thread",
        "summary": "Read a DM thread's messages",
        "description": "Returns a page of messages (newest first, up to 50) with the owner-tier projection (deleted/expired tombstones, free-tier per-thread lock). Attachments surface as `has_attachments` + `attachment_kinds` only — private token-gated URLs are never resolved here. Requires scope `dm:read`.\n\nCursor-paginated: pass the previous response's `next_cursor` back as `?cursor` to read older messages. Stop when `next_cursor` is `null`. The cursor is opaque.\n\nErrors: `400 INVALID_ARGUMENT` (malformed thread id/cursor); `404 NOT_PARTICIPANT` (owner not in the thread).",
        "tags": [
          "Direct messages"
        ],
        "x-required-scope": "dm:read",
        "responses": {
          "200": {
            "description": "Thread messages.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "messages",
                        "next_cursor"
                      ],
                      "properties": {
                        "messages": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/DmMessage"
                          }
                        },
                        "next_cursor": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "Opaque cursor for the next (older) page, or null at the start of the thread."
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "messages": [
                      {
                        "id": "msg_42",
                        "sender": "owner",
                        "sent_via": "agent",
                        "body": "Hey! Loved your synth setup.",
                        "created_at": 1737763200000,
                        "has_attachments": false,
                        "attachment_kinds": [],
                        "deleted_at": null,
                        "expired_at": null
                      }
                    ],
                    "next_cursor": null
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "The owner is not a participant in this thread. `code`: `NOT_PARTICIPANT`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/block": {
      "post": {
        "operationId": "block",
        "summary": "Block another user",
        "description": "Blocks a user by public `slug` on the owner's behalf (idempotent). Requires scope `safety:write`.\n\nErrors: `400 CANT_BLOCK_SELF` or `INVALID_ARGUMENT` (missing slug); `404 NOT_FOUND` (unknown slug).",
        "tags": [
          "Safety"
        ],
        "x-required-scope": "safety:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "slug"
                ],
                "properties": {
                  "slug": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "slug": "sam"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Blocked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "blocked"
                      ],
                      "properties": {
                        "blocked": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "blocked": true
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing slug or blocking yourself. `code`: `INVALID_ARGUMENT` or `CANT_BLOCK_SELF`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/report": {
      "post": {
        "operationId": "report",
        "summary": "Report another user",
        "description": "Reports a user by public `slug` with a classification. Subject to the SAME 5-per-owner-per-24h rate limit as a human. Set `also_block: true` to block in the same call. Requires scope `safety:write`.\n\nErrors: `400 CANT_REPORT_SELF`/`INVALID_ARGUMENT` (bad classification / missing slug / non-string or over-long comment); `404 NOT_FOUND` (unknown slug); `429 RATE_LIMITED`.",
        "tags": [
          "Safety"
        ],
        "x-required-scope": "safety:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "slug",
                  "classification"
                ],
                "properties": {
                  "slug": {
                    "type": "string"
                  },
                  "classification": {
                    "type": "string",
                    "enum": [
                      "inappropriate_photos",
                      "fake_profile",
                      "harassment",
                      "spam",
                      "underage",
                      "other"
                    ]
                  },
                  "comment": {
                    "type": "string",
                    "description": "Optional free text; trimmed + capped at 500 downstream."
                  },
                  "also_block": {
                    "type": "boolean",
                    "description": "Also block the user in the same call."
                  }
                }
              },
              "example": {
                "slug": "sam",
                "classification": "spam",
                "comment": "Sending unsolicited links.",
                "also_block": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reported (and blocked if requested).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "reported",
                        "blocked"
                      ],
                      "properties": {
                        "reported": {
                          "type": "boolean"
                        },
                        "blocked": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "reported": true,
                    "blocked": true
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad argument or reporting yourself. `code`: `INVALID_ARGUMENT` or `CANT_REPORT_SELF`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/notifications": {
      "get": {
        "operationId": "get_notifications",
        "summary": "Read the owner's notification inbox",
        "description": "Returns a page (20) of notifications with an unread breakdown and a pagination cursor. Read the next page by passing the previous response's `next_cursor` back as `?cursor`; stop when it is `null`. Requires scope `account:read`.\n\nErrors: `400 INVALID_ARGUMENT` (bad category or malformed cursor).",
        "tags": [
          "Notifications"
        ],
        "x-required-scope": "account:read",
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "dm",
                "other"
              ]
            },
            "description": "Filter; default all."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Opaque pagination cursor. Pass back the previous response's `next_cursor` verbatim to read the next page."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of notifications.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "notifications",
                        "unread_other_count",
                        "unread_dm_count",
                        "next_cursor"
                      ],
                      "properties": {
                        "notifications": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Notification"
                          }
                        },
                        "unread_other_count": {
                          "type": "integer"
                        },
                        "unread_dm_count": {
                          "type": "integer"
                        },
                        "next_cursor": {
                          "type": [
                            "number",
                            "null"
                          ],
                          "description": "createdAt-ms cursor for the next page, or null."
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "notifications": [
                      {
                        "id": "ntf_1",
                        "event_type": "rating.received",
                        "title": "New rating",
                        "body": "Someone rated you.",
                        "link": null,
                        "read_at": null,
                        "created_at": 1737763200000
                      }
                    ],
                    "unread_other_count": 3,
                    "unread_dm_count": 1,
                    "next_cursor": 1737763100000
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/webhooks": {
      "post": {
        "operationId": "register_webhook",
        "summary": "Register a webhook endpoint",
        "description": "Registers an endpoint for the given events. The endpoint is INERT (delivers nothing) until the owner approves it via an emailed magic link. The plaintext signing `secret` is returned exactly once here (never by list). The key must ALSO hold the read scope(s) covering the subscribed events (e.g. `dm:read` for `dm.*`, `approvals:write` for `approval.*`, `media:write` for `video.*`; `account:read` baseline). Requires scope `webhooks:write`.\n\nErrors: `400 INVALID_ARGUMENT`/`INVALID_URL`/`INVALID_EVENT`; `403 INSUFFICIENT_SCOPE` (missing content read scope); `409 WEBHOOK_LIMIT`.\n\nThis operation is idempotent when you send an `Idempotency-Key` header: a retry with the same key and the same body replays the original response for 24 hours; the same key with a different body returns 409 `IDEMPOTENCY_CONFLICT`.",
        "tags": [
          "Webhooks"
        ],
        "x-required-scope": "webhooks:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url",
                  "events"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "HTTPS delivery URL (SSRF-checked)."
                  },
                  "events": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string",
                      "enum": [
                        "rating.received",
                        "dm.message",
                        "dm.message_request",
                        "dm.request_accepted",
                        "payment.received",
                        "subscription.cancelled",
                        "subscription.expired",
                        "referral.rewarded",
                        "profile.published",
                        "approval.decided",
                        "video.scan_completed"
                      ]
                    }
                  }
                }
              },
              "example": {
                "url": "https://hooks.example.com/wydm",
                "events": [
                  "rating.received",
                  "dm.message"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Endpoint created (pending owner approval). The secret is returned once.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "id",
                        "url",
                        "events",
                        "secret",
                        "status"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "url": {
                          "type": "string"
                        },
                        "events": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "secret": {
                          "type": "string",
                          "description": "Plaintext signing secret — returned ONLY here."
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending_approval"
                          ]
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "id": "wh_1",
                    "url": "https://hooks.example.com/wydm",
                    "events": [
                      "rating.received",
                      "dm.message"
                    ],
                    "secret": "whsec_9f2a...",
                    "status": "pending_approval"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request. `code`: `INVALID_ARGUMENT`, `INVALID_URL`, or `INVALID_EVENT`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Insufficient scope for the webhook events (or agent paused, or Ultra lapsed). `code`: `INSUFFICIENT_SCOPE`, `AGENT_PAUSED`, or `ULTRA_REQUIRED`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Endpoint limit reached. `code`: `WEBHOOK_LIMIT`. Also `IDEMPOTENCY_CONFLICT` when an Idempotency-Key is reused with a different body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKeyHeader"
          }
        ]
      },
      "get": {
        "operationId": "list_webhooks",
        "summary": "List the owner's webhook endpoints",
        "description": "Lists endpoints. Never includes the signing secret. Requires scope `webhooks:write`.",
        "tags": [
          "Webhooks"
        ],
        "x-required-scope": "webhooks:write",
        "responses": {
          "200": {
            "description": "Webhook endpoints.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "webhooks"
                      ],
                      "properties": {
                        "webhooks": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/WebhookEndpoint"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "webhooks": [
                      {
                        "id": "wh_1",
                        "url": "https://hooks.example.com/wydm",
                        "events": [
                          "rating.received",
                          "dm.message"
                        ],
                        "created_at": 1737763200000,
                        "status": "active",
                        "source": "agent",
                        "approved_at": 1737763300000
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/webhooks/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "The webhook endpoint id."
        }
      ],
      "delete": {
        "operationId": "delete_webhook",
        "summary": "Delete a webhook endpoint",
        "description": "Revokes an endpoint by id. A foreign OR malformed id both map to `404 NOT_FOUND`. Requires scope `webhooks:write`.",
        "tags": [
          "Webhooks"
        ],
        "x-required-scope": "webhooks:write",
        "responses": {
          "200": {
            "description": "Revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "revoked"
                      ],
                      "properties": {
                        "revoked": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "revoked": true
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/approvals": {
      "post": {
        "operationId": "request_approval",
        "summary": "Request out-of-band human approval",
        "description": "Asks the human owner to approve or deny a sensitive action. WYDM mints a single-use magic-link token (only its hash is stored) and emails the owner the link. The raw token is NEVER returned — it rides ONLY the emailed link. Requires scope `approvals:write`.\n\nValidation: `action` ≤64 chars, `summary` ≤280 chars, `context` JSON ≤2KB, `ttl_seconds` clamped to 60..3600.\n\nErrors: `400 INVALID_APPROVAL`; `429 APPROVAL_LIMIT` (too many pending).\n\nThis operation is idempotent when you send an `Idempotency-Key` header: a retry with the same key and the same body replays the original response for 24 hours; the same key with a different body returns 409 `IDEMPOTENCY_CONFLICT`.",
        "tags": [
          "Approvals"
        ],
        "x-required-scope": "approvals:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "action",
                  "summary"
                ],
                "properties": {
                  "action": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "Short machine-ish action slug/label."
                  },
                  "summary": {
                    "type": "string",
                    "maxLength": 280,
                    "description": "One-line human-readable description."
                  },
                  "context": {
                    "type": "object",
                    "description": "Optional context blob; serialized JSON must be ≤2KB.",
                    "additionalProperties": true
                  },
                  "ttl_seconds": {
                    "type": "number",
                    "description": "Approval lifetime; clamped to 60..3600. Default ~15 min."
                  }
                }
              },
              "example": {
                "action": "send_gift",
                "summary": "Send a $20 gift card to Sam?",
                "context": {
                  "recipient": "sam",
                  "amount_usd": 20
                },
                "ttl_seconds": 900
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Approval request created; the owner has been emailed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "id",
                        "status",
                        "expires_at"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending"
                          ]
                        },
                        "expires_at": {
                          "type": "number",
                          "description": "ms-epoch expiry."
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "id": "apr_5",
                    "status": "pending",
                    "expires_at": 1737764100000
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid approval request. `code`: `INVALID_APPROVAL`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "429": {
            "description": "Too many pending approvals. `code`: `APPROVAL_LIMIT`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKeyHeader"
          }
        ]
      }
    },
    "/approvals/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "The approval request id."
        }
      ],
      "get": {
        "operationId": "get_approval",
        "summary": "Poll an approval verdict",
        "description": "Reads the current verdict for one approval request. Ownership-scoped: a foreign or non-existent id both map to `404 APPROVAL_NOT_FOUND` (no oracle). Requires scope `approvals:write`.",
        "tags": [
          "Approvals"
        ],
        "x-required-scope": "approvals:write",
        "responses": {
          "200": {
            "description": "The approval request and its status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Approval"
                    }
                  }
                },
                "example": {
                  "data": {
                    "id": "apr_5",
                    "action": "send_gift",
                    "summary": "Send a $20 gift card to Sam?",
                    "status": "approved",
                    "created_at": 1737763200000,
                    "expires_at": 1737764100000,
                    "decided_at": 1737763500000
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such approval (unknown, foreign, or malformed id). `code`: `APPROVAL_NOT_FOUND`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "rating.received": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "webhook_rating_received",
        "summary": "Someone rated the owner's profile (with or without a note).",
        "description": "Fires for EVERY rating another user submits on the owner's profile — a yes/no verdict, with or without a moderation-cleared note. Noted and no-note ratings consolidate into this one event.\n\nThe `title` is `\"New rating\"` for a rating with no note, or `\"New rating with note\"` for a noted rating. The `link` is `/ratings`. The push `body` is empty for a no-note rating; for a noted rating it is the note text truncated to 100 characters. `data.note` always carries the rater's FULL note text, or an empty string (`\"\"`) when the rating has no note.\n\nDedupe differs by note. A no-note rating is daily-deduped — a burst of ratings collapses to one delivery per day (the in-app inbox bumps a count) — and its `dedupe_key` is a per-owner-per-day key. A noted rating deduplicates per rating, so each cleared note delivers, and its `dedupe_key` is the rating id.\n\nSubscribing requires `webhooks:write` (to register the endpoint) plus these per-event read scopes: `account:read`.",
        "x-required-scopes": [
          "account:read"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The signed webhook envelope. Verify `X-Signature` over the raw body before processing.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "The `rating.received` webhook envelope — the base envelope plus event-specific fields in `data`.",
                "required": [
                  "id",
                  "event",
                  "occurred_at",
                  "data"
                ],
                "properties": {
                  "id": {
                    "$ref": "#/components/schemas/WebhookEnvelope/properties/id"
                  },
                  "event": {
                    "$ref": "#/components/schemas/WebhookEnvelope/properties/event"
                  },
                  "occurred_at": {
                    "$ref": "#/components/schemas/WebhookEnvelope/properties/occurred_at"
                  },
                  "data": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/WebhookEventData"
                      },
                      {
                        "type": "object",
                        "required": [
                          "note"
                        ],
                        "properties": {
                          "note": {
                            "type": "string",
                            "description": "The rater's note. Empty string when the rating has no note."
                          }
                        }
                      }
                    ]
                  }
                }
              },
              "example": {
                "id": "wd_3f8a1c2e9b7d",
                "event": "rating.received",
                "occurred_at": 1753500000000,
                "data": {
                  "title": "New rating with note",
                  "body": "You seem really thoughtful, would love to chat!",
                  "link": "/ratings",
                  "dedupe_key": "rating_4b2a9c1d",
                  "note": "You seem really thoughtful, would love to chat!"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx acknowledges receipt. A non-2xx (or no response) is retried with exponential backoff, up to 10 attempts (~17 minute window)."
          }
        }
      }
    },
    "dm.message": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "webhook_dm_message",
        "summary": "The owner received a direct message.",
        "description": "Fires when another user sends the owner a message in an accepted (or already-open) thread.\n\n`title` is the sender's display name (fallback `\"New message\"`), `body` is up to a 200-character preview of the message text — or an attachment summary such as `\"📷 Photo\"`, `\"🎥 Video\"`, `\"🎤 Voice note\"`, `\"📄 <filename>\"`, or `\"📎 N attachments\"` — and `link` is `/dm/{threadId}`.\n\nSubscribing requires `webhooks:write` (to register the endpoint) plus these per-event read scopes: `account:read`, `dm:read`.",
        "x-required-scopes": [
          "account:read",
          "dm:read"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The signed webhook envelope. Verify `X-Signature` over the raw body before processing.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              },
              "example": {
                "id": "wd_3f8a1c2e9b7d",
                "event": "dm.message",
                "occurred_at": 1753500000000,
                "data": {
                  "title": "Alex Rivera",
                  "body": "hey! saw we matched on hiking — favourite trail?",
                  "link": "/dm/th_4c9e",
                  "dedupe_key": "dm_msg_7a1b"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx acknowledges receipt. A non-2xx (or no response) is retried with exponential backoff, up to 10 attempts (~17 minute window)."
          }
        }
      }
    },
    "dm.message_request": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "webhook_dm_message_request",
        "summary": "The owner received a new message request.",
        "description": "Fires when a brand-new pending message-request thread is opened to the owner (distinct from an ordinary message so it reads as a request).\n\n`title` is the sender's display name, `body` is `\"Sent you a message request\"`, `link` is `/dm/{threadId}`.\n\nSubscribing requires `webhooks:write` (to register the endpoint) plus these per-event read scopes: `account:read`, `dm:read`.",
        "x-required-scopes": [
          "account:read",
          "dm:read"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The signed webhook envelope. Verify `X-Signature` over the raw body before processing.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              },
              "example": {
                "id": "wd_3f8a1c2e9b7d",
                "event": "dm.message_request",
                "occurred_at": 1753500000000,
                "data": {
                  "title": "Jordan Lee",
                  "body": "Sent you a message request",
                  "link": "/dm/th_88a1",
                  "dedupe_key": "dm_msg_5f2c"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx acknowledges receipt. A non-2xx (or no response) is retried with exponential backoff, up to 10 attempts (~17 minute window)."
          }
        }
      }
    },
    "dm.request_accepted": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "webhook_dm_request_accepted",
        "summary": "A message request the owner sent was accepted.",
        "description": "Fires when the recipient of a pending request the owner initiated accepts it; delivered to the owner (the initiator).\n\n`title` is the accepter's display name, `body` is `\"Accepted your message request\"`, `link` is `/dm/{threadId}`.\n\nSubscribing requires `webhooks:write` (to register the endpoint) plus these per-event read scopes: `account:read`, `dm:read`.",
        "x-required-scopes": [
          "account:read",
          "dm:read"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The signed webhook envelope. Verify `X-Signature` over the raw body before processing.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              },
              "example": {
                "id": "wd_3f8a1c2e9b7d",
                "event": "dm.request_accepted",
                "occurred_at": 1753500000000,
                "data": {
                  "title": "Sam Park",
                  "body": "Accepted your message request",
                  "link": "/dm/th_2b7d",
                  "dedupe_key": "request-accepted:th_2b7d"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx acknowledges receipt. A non-2xx (or no response) is retried with exponential backoff, up to 10 attempts (~17 minute window)."
          }
        }
      }
    },
    "payment.received": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "webhook_payment_received",
        "summary": "The owner's premium was activated.",
        "description": "Fires on the FIRST premium activation from a payment — web billing first activation, or a RevenueCat INITIAL_PURCHASE / RENEWAL / UNCANCELLATION / PRODUCT_CHANGE. Silent rebills do not re-fire.\n\n`title` is `\"Premium activated\"`, `body` is an empty string, `link` is `/settings`.\n\nSubscribing requires `webhooks:write` (to register the endpoint) plus these per-event read scopes: `account:read`.",
        "x-required-scopes": [
          "account:read"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The signed webhook envelope. Verify `X-Signature` over the raw body before processing.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              },
              "example": {
                "id": "wd_3f8a1c2e9b7d",
                "event": "payment.received",
                "occurred_at": 1753500000000,
                "data": {
                  "title": "Premium activated",
                  "body": "",
                  "link": "/settings",
                  "dedupe_key": "rc:ent_premium:INITIAL_PURCHASE:1753500000000"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx acknowledges receipt. A non-2xx (or no response) is retried with exponential backoff, up to 10 attempts (~17 minute window)."
          }
        }
      }
    },
    "subscription.cancelled": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "webhook_subscription_cancelled",
        "summary": "The owner cancelled their subscription (access continues until period end).",
        "description": "Fires when a subscription is cancelled. Access remains until the current period end — this is the cancel signal, not the end of access.\n\n`title` is `\"Subscription cancelled\"`, `body` is an empty string, `link` is `/settings`.\n\nSubscribing requires `webhooks:write` (to register the endpoint) plus these per-event read scopes: `account:read`.",
        "x-required-scopes": [
          "account:read"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The signed webhook envelope. Verify `X-Signature` over the raw body before processing.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              },
              "example": {
                "id": "wd_3f8a1c2e9b7d",
                "event": "subscription.cancelled",
                "occurred_at": 1753500000000,
                "data": {
                  "title": "Subscription cancelled",
                  "body": "",
                  "link": "/settings",
                  "dedupe_key": "rc:ent_premium:CANCELLATION:1753500000000"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx acknowledges receipt. A non-2xx (or no response) is retried with exponential backoff, up to 10 attempts (~17 minute window)."
          }
        }
      }
    },
    "subscription.expired": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "webhook_subscription_expired",
        "summary": "The owner's premium access ended.",
        "description": "Fires when premium access actually ends (natural expiry, refund, or an entitlement flipping to free).\n\n`title` is `\"Your premium plan has ended\"`, `body` is an empty string, `link` is `/settings`.\n\nSubscribing requires `webhooks:write` (to register the endpoint) plus these per-event read scopes: `account:read`.",
        "x-required-scopes": [
          "account:read"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The signed webhook envelope. Verify `X-Signature` over the raw body before processing.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              },
              "example": {
                "id": "wd_3f8a1c2e9b7d",
                "event": "subscription.expired",
                "occurred_at": 1753500000000,
                "data": {
                  "title": "Your premium plan has ended",
                  "body": "",
                  "link": "/settings",
                  "dedupe_key": "rc:ent_premium:EXPIRATION:1753500000000"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx acknowledges receipt. A non-2xx (or no response) is retried with exponential backoff, up to 10 attempts (~17 minute window)."
          }
        }
      }
    },
    "referral.rewarded": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "webhook_referral_rewarded",
        "summary": "The owner earned a free premium week from a referral.",
        "description": "Fires when a friend the owner invited publishes their profile AND a free week is actually granted (suppressed at the 8-week referral cap).\n\n`title` is `\"You earned a free week of Premium!\"`, `body` is `\"A friend you invited just published their profile.\"`, `link` is `/settings`.\n\nSubscribing requires `webhooks:write` (to register the endpoint) plus these per-event read scopes: `account:read`.",
        "x-required-scopes": [
          "account:read"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The signed webhook envelope. Verify `X-Signature` over the raw body before processing.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              },
              "example": {
                "id": "wd_3f8a1c2e9b7d",
                "event": "referral.rewarded",
                "occurred_at": 1753500000000,
                "data": {
                  "title": "You earned a free week of Premium!",
                  "body": "A friend you invited just published their profile.",
                  "link": "/settings",
                  "dedupe_key": "referral:rewarded:u_friend"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx acknowledges receipt. A non-2xx (or no response) is retried with exponential backoff, up to 10 attempts (~17 minute window)."
          }
        }
      }
    },
    "profile.published": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "webhook_profile_published",
        "summary": "The owner's profile was published for the first time.",
        "description": "Fires on the owner's FIRST profile publish (the already-published path short-circuits, so it never re-fires).\n\n`title` is `\"Your profile is live\"`, `body` is `\"Your WYDM profile is now published.\"`, `link` is `/p/{slug}`.\n\nSubscribing requires `webhooks:write` (to register the endpoint) plus these per-event read scopes: `account:read`.",
        "x-required-scopes": [
          "account:read"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The signed webhook envelope. Verify `X-Signature` over the raw body before processing.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              },
              "example": {
                "id": "wd_3f8a1c2e9b7d",
                "event": "profile.published",
                "occurred_at": 1753500000000,
                "data": {
                  "title": "Your profile is live",
                  "body": "Your WYDM profile is now published.",
                  "link": "/p/alex-r",
                  "dedupe_key": "published:pr_owner"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx acknowledges receipt. A non-2xx (or no response) is retried with exponential backoff, up to 10 attempts (~17 minute window)."
          }
        }
      }
    },
    "approval.decided": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "webhook_approval_decided",
        "summary": "The owner approved or denied a pending agent approval request.",
        "description": "Fires when the owner approves or denies a pending approval request via the out-of-band email flow. This is a webhook-ONLY event (never a push notification).\n\n`title` is `\"Approval approved\"` or `\"Approval denied\"`, `body` is the approval's summary text, and there is no `link`. `data` additionally carries `approval_id`, `action`, `status` (`\"approved\"`|`\"denied\"`), and `decided_at`.\n\nSubscribing requires `webhooks:write` (to register the endpoint) plus these per-event read scopes: `account:read`, `approvals:write`.",
        "x-required-scopes": [
          "account:read",
          "approvals:write"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The signed webhook envelope. Verify `X-Signature` over the raw body before processing.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "The `approval.decided` webhook envelope — the base envelope plus event-specific fields in `data`.",
                "required": [
                  "id",
                  "event",
                  "occurred_at",
                  "data"
                ],
                "properties": {
                  "id": {
                    "$ref": "#/components/schemas/WebhookEnvelope/properties/id"
                  },
                  "event": {
                    "$ref": "#/components/schemas/WebhookEnvelope/properties/event"
                  },
                  "occurred_at": {
                    "$ref": "#/components/schemas/WebhookEnvelope/properties/occurred_at"
                  },
                  "data": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/WebhookEventData"
                      },
                      {
                        "type": "object",
                        "required": [
                          "approval_id",
                          "action",
                          "status",
                          "decided_at"
                        ],
                        "properties": {
                          "approval_id": {
                            "type": "string",
                            "description": "The approval request's id."
                          },
                          "action": {
                            "type": "string",
                            "description": "The action the agent had requested approval for."
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "approved",
                              "denied"
                            ],
                            "description": "The owner's verdict."
                          },
                          "decided_at": {
                            "type": "number",
                            "description": "Unix epoch milliseconds when the owner decided."
                          }
                        }
                      }
                    ]
                  }
                }
              },
              "example": {
                "id": "wd_3f8a1c2e9b7d",
                "event": "approval.decided",
                "occurred_at": 1753500000000,
                "data": {
                  "title": "Approval approved",
                  "body": "Send DM to Jordan Lee: \"Would you like to grab coffee this weekend?\"",
                  "link": null,
                  "dedupe_key": "approval-decided:ap_9c1f",
                  "approval_id": "ap_9c1f",
                  "action": "dm.send",
                  "status": "approved",
                  "decided_at": 1753500000000
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx acknowledges receipt. A non-2xx (or no response) is retried with exponential backoff, up to 10 attempts (~17 minute window)."
          }
        }
      }
    },
    "video.scan_completed": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "webhook_video_scan_completed",
        "summary": "An uploaded profile video finished NSFW scanning.",
        "description": "Fires exactly once when an uploaded profile video reaches a terminal scan state. This is a webhook-ONLY event (never a push notification).\n\n`title` is `\"Video scan complete\"`, `body` is the terminal state string, and there is no `link`. `data` additionally carries `video_id`, `state` (`\"ready\"`|`\"rejected_nsfw\"`|`\"error\"`), and `rejected_reason` (only when rejected).\n\nSubscribing requires `webhooks:write` (to register the endpoint) plus these per-event read scopes: `account:read`, `media:write`.",
        "x-required-scopes": [
          "account:read",
          "media:write"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The signed webhook envelope. Verify `X-Signature` over the raw body before processing.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "The `video.scan_completed` webhook envelope — the base envelope plus event-specific fields in `data`.",
                "required": [
                  "id",
                  "event",
                  "occurred_at",
                  "data"
                ],
                "properties": {
                  "id": {
                    "$ref": "#/components/schemas/WebhookEnvelope/properties/id"
                  },
                  "event": {
                    "$ref": "#/components/schemas/WebhookEnvelope/properties/event"
                  },
                  "occurred_at": {
                    "$ref": "#/components/schemas/WebhookEnvelope/properties/occurred_at"
                  },
                  "data": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/WebhookEventData"
                      },
                      {
                        "type": "object",
                        "required": [
                          "video_id",
                          "state"
                        ],
                        "properties": {
                          "video_id": {
                            "type": "string",
                            "description": "The scanned profile video's id."
                          },
                          "state": {
                            "type": "string",
                            "enum": [
                              "ready",
                              "rejected_nsfw",
                              "error"
                            ],
                            "description": "Terminal scan outcome: `ready` (visible to viewers), `rejected_nsfw` (flagged; clip bytes deleted), or `error` (unscannable; owner should re-upload)."
                          },
                          "rejected_reason": {
                            "type": "string",
                            "description": "Present only when the scan rejected the clip; the rejection reason."
                          }
                        }
                      }
                    ]
                  }
                }
              },
              "example": {
                "id": "wd_3f8a1c2e9b7d",
                "event": "video.scan_completed",
                "occurred_at": 1753500000000,
                "data": {
                  "title": "Video scan complete",
                  "body": "rejected_nsfw",
                  "link": null,
                  "dedupe_key": "video-scan:vid_2a7e",
                  "video_id": "vid_2a7e",
                  "state": "rejected_nsfw",
                  "rejected_reason": "explicit_nudity"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx acknowledges receipt. A non-2xx (or no response) is retried with exponential backoff, up to 10 attempts (~17 minute window)."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "agentKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "wydm_agent_…",
        "description": "Agent API key issued from the WYDM developer console. Send as `Authorization: Bearer wydm_agent_…`. The key resolves to exactly one owner user and carries a fixed set of scopes."
      }
    },
    "headers": {
      "XRequestId": {
        "description": "Correlation id for this response. Present on every response (success and error). Quote it in a support request so the team can find the log line. The same id appears as the `instance` URN in an error body.",
        "schema": {
          "type": "string"
        }
      },
      "RetryAfter": {
        "description": "Present on a 429 (RATE_LIMITED) response. A whole number of seconds to wait before retrying.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Authentication failed. `code`: `MISSING_BEARER`, `INVALID_TOKEN`, `TOKEN_REVOKED`, or `ACCOUNT_NOT_FOUND`.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "https://wydm.app/agent/errors/invalid_token",
              "title": "Invalid token",
              "status": 401,
              "detail": "Token invalid.",
              "code": "INVALID_TOKEN"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Authorization failed. `code`: `INSUFFICIENT_SCOPE`, `AGENT_PAUSED`, `ULTRA_REQUIRED`, `ACCOUNT_SUSPENDED`, or `ACCOUNT_SOFT_DELETED`.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "https://wydm.app/agent/errors/insufficient_scope",
              "title": "Insufficient scope",
              "status": 403,
              "detail": "This key is missing the 'profile:write' scope.",
              "code": "INSUFFICIENT_SCOPE"
            }
          }
        }
      },
      "BadRequest": {
        "description": "Malformed request. `code`: `INVALID_ARGUMENT`.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "https://wydm.app/agent/errors/invalid_argument",
              "title": "Invalid argument",
              "status": 400,
              "detail": "Body must be a JSON object.",
              "code": "INVALID_ARGUMENT"
            }
          }
        }
      },
      "NotFound": {
        "description": "Target not found. `code`: `NOT_FOUND`.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "https://wydm.app/agent/errors/not_found",
              "title": "Not found",
              "status": 404,
              "detail": "No such profile: sam",
              "code": "NOT_FOUND"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit reached. `code`: `RATE_LIMITED`. Carries a `Retry-After` header (whole seconds).",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "https://wydm.app/agent/errors/rate_limited",
              "title": "Rate limit reached",
              "status": 429,
              "detail": "Daily rating cap reached.",
              "code": "RATE_LIMITED",
              "instance": "urn:wydm:request:9f3c7a2e5b1d4c6f8a0e2d4b6c8f0a1e"
            }
          }
        }
      },
      "ModerationUnavailable": {
        "description": "Media moderation is not configured on this deployment (fail-closed). `code`: `MODERATION_UNAVAILABLE`.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "https://wydm.app/agent/errors/moderation_unavailable",
              "title": "Moderation unavailable",
              "status": 503,
              "detail": "Media moderation is not configured on this deployment.",
              "code": "MODERATION_UNAVAILABLE"
            }
          }
        }
      },
      "IdempotencyConflict": {
        "description": "The Idempotency-Key was reused with a different request body. `code`: `IDEMPOTENCY_CONFLICT`.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "https://wydm.app/agent/errors/idempotency_conflict",
              "title": "Idempotency-Key conflict",
              "status": 409,
              "detail": "This Idempotency-Key was already used with a different request body.",
              "code": "IDEMPOTENCY_CONFLICT"
            }
          }
        }
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem document (`application/problem+json`) returned by every error response.",
        "required": [
          "type",
          "title",
          "status",
          "detail",
          "code"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "description": "Stable URN per code, e.g. https://wydm.app/agent/errors/invalid_token."
          },
          "title": {
            "type": "string",
            "description": "Short human-readable summary of the error class."
          },
          "status": {
            "type": "integer",
            "description": "HTTP status code."
          },
          "detail": {
            "type": "string",
            "description": "Human-readable explanation specific to this occurrence."
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable error code (switch on this)."
          },
          "instance": {
            "type": "string",
            "format": "uri",
            "description": "Per-response correlation URN (`urn:wydm:request:<id>`). The `<id>` is the same value as the `X-Request-Id` header on this response. Quote it in a support request."
          }
        },
        "example": {
          "type": "https://wydm.app/agent/errors/needs_profile",
          "title": "Published profile required",
          "status": 409,
          "detail": "You need a profile before this can be read.",
          "code": "NEEDS_PROFILE",
          "instance": "urn:wydm:request:9f3c7a2e5b1d4c6f8a0e2d4b6c8f0a1e"
        }
      },
      "Profile": {
        "type": "object",
        "description": "The owner's profile (snake_case). Nullable fields are null when unset.",
        "required": [
          "id",
          "user_id",
          "display_name",
          "bio",
          "looking_for",
          "languages",
          "vices",
          "dating_intentions",
          "prompts",
          "visibility",
          "published",
          "created_at",
          "updated_at",
          "search_indexing"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "user_id": {
            "type": "string"
          },
          "display_name": {
            "type": "string"
          },
          "bio": {
            "type": "string"
          },
          "gender": {
            "type": [
              "string",
              "null"
            ]
          },
          "looking_for": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "education": {
            "type": [
              "string",
              "null"
            ]
          },
          "education_level": {
            "type": [
              "string",
              "null"
            ]
          },
          "job": {
            "type": [
              "string",
              "null"
            ]
          },
          "hometown": {
            "type": [
              "string",
              "null"
            ]
          },
          "religion": {
            "type": [
              "string",
              "null"
            ]
          },
          "politics": {
            "type": [
              "string",
              "null"
            ]
          },
          "languages": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "height": {
            "type": [
              "number",
              "null"
            ],
            "description": "Height in cm."
          },
          "vices": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "dating_intentions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "prompts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PromptAnswer"
            }
          },
          "theme_song": {
            "$ref": "#/components/schemas/ThemeSong"
          },
          "location": {
            "$ref": "#/components/schemas/Location"
          },
          "live_location": {
            "$ref": "#/components/schemas/LiveLocation"
          },
          "slug": {
            "type": [
              "string",
              "null"
            ]
          },
          "visibility": {
            "type": "string",
            "enum": [
              "private",
              "link_only",
              "public"
            ]
          },
          "published": {
            "type": "boolean"
          },
          "published_at": {
            "type": [
              "number",
              "null"
            ],
            "description": "ms-epoch."
          },
          "public_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "search_indexing": {
            "type": "boolean",
            "description": "Whether the owner opted in to search-engine indexing. Off by default."
          },
          "search_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "The indexable `/pub/<slug>` URL while search engines are being served the profile (opted in, published, not private). Null otherwise. `public_url` (`/p/<slug>`) is never indexed."
          },
          "created_at": {
            "type": "number",
            "description": "ms-epoch."
          },
          "updated_at": {
            "type": "number",
            "description": "ms-epoch."
          }
        }
      },
      "PromptAnswer": {
        "type": "object",
        "required": [
          "prompt_id",
          "category",
          "prompt_text",
          "answer",
          "voice"
        ],
        "properties": {
          "prompt_id": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "prompt_text": {
            "type": "string"
          },
          "answer": {
            "type": [
              "string",
              "null"
            ]
          },
          "voice": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "storage_key": {
                "type": "string"
              },
              "url": {
                "type": "string"
              },
              "duration_ms": {
                "type": "number"
              },
              "mime_type": {
                "type": "string"
              }
            }
          }
        }
      },
      "ThemeSong": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "track_id": {
            "type": "string"
          },
          "track_name": {
            "type": "string"
          },
          "artist_name": {
            "type": "string"
          },
          "album_art_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "duration_ms": {
            "type": "number"
          },
          "spotify_url": {
            "type": "string"
          }
        }
      },
      "Location": {
        "type": [
          "object",
          "null"
        ],
        "description": "The owner's stored location (owner-only fields).",
        "properties": {
          "latitude": {
            "type": "number"
          },
          "longitude": {
            "type": "number"
          },
          "city": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "country_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "type": "string"
          },
          "updated_at": {
            "type": "number",
            "description": "ms-epoch."
          }
        }
      },
      "LiveLocation": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "latitude": {
            "type": "number"
          },
          "longitude": {
            "type": "number"
          },
          "updated_at": {
            "type": "number",
            "description": "ms-epoch."
          }
        }
      },
      "ProfilePatch": {
        "type": "object",
        "description": "Patch body for update_profile — include only the keys to change. Explicit null clears a nullable field.",
        "properties": {
          "bio": {
            "type": "string"
          },
          "education": {
            "type": "string"
          },
          "job": {
            "type": "string"
          },
          "hometown": {
            "type": "string"
          },
          "religion": {
            "type": "string"
          },
          "gender": {
            "type": "string"
          },
          "education_level": {
            "type": "string"
          },
          "politics": {
            "type": "string"
          },
          "display_name": {
            "type": "string",
            "description": "Stored on the user row."
          },
          "looking_for": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "languages": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "vices": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "dating_intentions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "height": {
            "type": [
              "number",
              "null"
            ]
          },
          "location": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "theme_song": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "prompt_answers": {
            "type": "array",
            "description": "Text answers only; any voice sub-object is stripped. The id field is `prompt_id`. An entry with no usable prompt id is a `400 INVALID_ARGUMENT`.",
            "items": {
              "type": "object",
              "required": [
                "prompt_id"
              ],
              "properties": {
                "prompt_id": {
                  "type": "string"
                },
                "answer": {
                  "type": "string"
                }
              }
            }
          },
          "search_indexing": {
            "type": "boolean",
            "description": "Opt in to (true) or out of (false) search-engine indexing at `/pub/<slug>`. Opting in needs a published, non-private profile that passes publish readiness, else `409 NOT_PUBLISHABLE`. Making the profile private turns it off."
          }
        }
      },
      "MediaItem": {
        "type": "object",
        "description": "A photo or video on the owner's profile, with moderation state.",
        "required": [
          "id",
          "position",
          "url",
          "type",
          "state",
          "rejected_reason"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "position": {
            "type": "number"
          },
          "url": {
            "type": [
              "string",
              "null"
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "photo",
              "video"
            ]
          },
          "state": {
            "type": "string",
            "description": "e.g. published, scanning_nsfw, rejected_nsfw, csam_match (photos) or uploading, scanning_nsfw, ready, rejected_nsfw, error (videos)."
          },
          "rejected_reason": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ReadinessRule": {
        "type": "object",
        "required": [
          "id",
          "label",
          "passed"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable machine key, e.g. email_verified, has_photo."
          },
          "label": {
            "type": "string",
            "description": "Human-readable copy."
          },
          "passed": {
            "type": "boolean"
          }
        }
      },
      "FeedCard": {
        "type": "object",
        "required": [
          "slug",
          "display_name",
          "first_name",
          "age",
          "lead_photo_url",
          "distance_km"
        ],
        "properties": {
          "slug": {
            "type": "string"
          },
          "display_name": {
            "type": "string"
          },
          "first_name": {
            "type": "string"
          },
          "age": {
            "type": [
              "integer",
              "null"
            ]
          },
          "lead_photo_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "distance_km": {
            "type": "number"
          },
          "previous_verdict": {
            "type": "string",
            "enum": [
              "yes",
              "no"
            ],
            "description": "Present only if the owner previously rated this profile."
          }
        }
      },
      "LiveMarker": {
        "type": "object",
        "description": "A jittered privacy-offset live-map marker — never the target's raw location.",
        "required": [
          "slug",
          "first_name",
          "age",
          "lead_photo_url",
          "jittered_lat",
          "jittered_lng",
          "online",
          "checked_in_minutes_ago"
        ],
        "properties": {
          "slug": {
            "type": "string"
          },
          "first_name": {
            "type": "string"
          },
          "age": {
            "type": [
              "integer",
              "null"
            ]
          },
          "lead_photo_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "jittered_lat": {
            "type": "number"
          },
          "jittered_lng": {
            "type": "number"
          },
          "online": {
            "type": "boolean"
          },
          "checked_in_minutes_ago": {
            "type": "number"
          }
        }
      },
      "DmThreadSummary": {
        "type": "object",
        "description": "A trimmed accepted-inbox thread summary (list_dms).",
        "required": [
          "thread_id",
          "other",
          "last_message_at",
          "request_state",
          "is_initiator",
          "unread_count"
        ],
        "properties": {
          "thread_id": {
            "type": "string"
          },
          "other": {
            "type": "object",
            "required": [
              "slug",
              "display_name"
            ],
            "properties": {
              "slug": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "display_name": {
                "type": "string"
              }
            }
          },
          "last_message_at": {
            "type": "number",
            "description": "ms-epoch."
          },
          "request_state": {
            "type": "string",
            "enum": [
              "pending",
              "accepted"
            ]
          },
          "is_initiator": {
            "type": "boolean"
          },
          "unread_count": {
            "type": "integer"
          }
        }
      },
      "DmRequestThread": {
        "type": "object",
        "description": "A pending inbound message-request thread (list_requests).",
        "required": [
          "id",
          "other_user",
          "last_message",
          "unread_count",
          "initiator_user_id",
          "blocked_by_user_id",
          "request_state",
          "created_at",
          "last_message_at",
          "pinned_at",
          "muted_at",
          "other_typing_expires_at"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "other_user": {
            "$ref": "#/components/schemas/DmParty"
          },
          "last_message": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "sender_id": {
                "type": "string"
              },
              "body": {
                "type": "string"
              },
              "read_at": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "created_at": {
                "type": "number"
              },
              "deleted_at": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "expired_at": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "attachment_kind": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "attachment_duration_ms": {
                "type": [
                  "number",
                  "null"
                ]
              }
            }
          },
          "unread_count": {
            "type": "integer"
          },
          "initiator_user_id": {
            "type": "string"
          },
          "blocked_by_user_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "request_state": {
            "type": "string",
            "enum": [
              "pending",
              "accepted"
            ]
          },
          "created_at": {
            "type": "number"
          },
          "last_message_at": {
            "type": "number"
          },
          "pinned_at": {
            "type": [
              "number",
              "null"
            ]
          },
          "muted_at": {
            "type": [
              "number",
              "null"
            ]
          },
          "other_typing_expires_at": {
            "type": [
              "number",
              "null"
            ]
          }
        }
      },
      "DmParty": {
        "type": "object",
        "required": [
          "user_id",
          "display_name",
          "avatar_url",
          "slug"
        ],
        "properties": {
          "user_id": {
            "type": "string"
          },
          "display_name": {
            "type": "string"
          },
          "avatar_url": {
            "type": "string"
          },
          "slug": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "DmMessage": {
        "type": "object",
        "description": "A message in a thread (owner-tier projection; attachment URLs are never resolved for agents).",
        "required": [
          "id",
          "sender",
          "sent_via",
          "body",
          "created_at",
          "has_attachments",
          "attachment_kinds",
          "deleted_at",
          "expired_at"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "sender": {
            "type": "string",
            "enum": [
              "owner",
              "other"
            ]
          },
          "sent_via": {
            "type": "string",
            "enum": [
              "human",
              "agent"
            ],
            "description": "How the message was sent; drives the disclosure badge."
          },
          "body": {
            "type": "string",
            "description": "Empty for deleted/locked rows."
          },
          "created_at": {
            "type": "number"
          },
          "has_attachments": {
            "type": "boolean"
          },
          "attachment_kinds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "deleted_at": {
            "type": [
              "number",
              "null"
            ]
          },
          "expired_at": {
            "type": [
              "number",
              "null"
            ]
          }
        }
      },
      "Notification": {
        "type": "object",
        "required": [
          "id",
          "event_type",
          "title",
          "body",
          "link",
          "read_at",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "event_type": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "body": {
            "type": "string"
          },
          "link": {
            "type": [
              "string",
              "null"
            ]
          },
          "read_at": {
            "type": [
              "number",
              "null"
            ]
          },
          "created_at": {
            "type": "number"
          }
        }
      },
      "WebhookEndpoint": {
        "type": "object",
        "description": "A webhook endpoint (list_webhooks). The signing secret is never included.",
        "required": [
          "id",
          "url",
          "events",
          "created_at",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "created_at": {
            "type": "number",
            "description": "ms-epoch."
          },
          "revoked_at": {
            "type": "number",
            "description": "ms-epoch; present when revoked."
          },
          "last_delivery_at": {
            "type": "number",
            "description": "ms-epoch; present after first delivery attempt."
          },
          "last_status": {
            "type": "string",
            "description": "Last delivery status."
          },
          "failure_count": {
            "type": "integer",
            "description": "Consecutive failed delivery attempts; any success resets it to 0."
          },
          "disabled_at": {
            "type": "number",
            "description": "ms-epoch; present when the platform auto-disabled the endpoint after ~20 consecutive failed deliveries. A disabled endpoint delivers nothing — re-register to re-enable."
          },
          "disabled_reason": {
            "type": "string",
            "description": "Short machine label for the auto-disable, e.g. \"consecutive_failures\"."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "pending_approval",
              "revoked"
            ],
            "description": "Derived: revoked > pending_approval > active."
          },
          "source": {
            "type": "string",
            "enum": [
              "agent",
              "console"
            ]
          },
          "approved_at": {
            "type": "number",
            "description": "ms-epoch; present once the owner approves."
          }
        }
      },
      "Approval": {
        "type": "object",
        "description": "An out-of-band approval request and its verdict.",
        "required": [
          "id",
          "action",
          "summary",
          "status",
          "created_at",
          "expires_at"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "action": {
            "type": "string"
          },
          "summary": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "denied",
              "expired"
            ]
          },
          "created_at": {
            "type": "number",
            "description": "ms-epoch."
          },
          "expires_at": {
            "type": "number",
            "description": "ms-epoch."
          },
          "decided_at": {
            "type": "number",
            "description": "ms-epoch; present only once a verdict is reached."
          }
        }
      },
      "WebhookEventData": {
        "type": "object",
        "description": "The `data` object common to every webhook event. Typed events (e.g. approval.decided, video.scan_completed) merge extra fields alongside these.",
        "required": [
          "title",
          "body",
          "dedupe_key"
        ],
        "properties": {
          "title": {
            "type": "string",
            "description": "Short human-readable headline for the event."
          },
          "body": {
            "type": "string",
            "description": "Human-readable detail (may be an empty string for events that carry all their meaning in the title)."
          },
          "link": {
            "type": [
              "string",
              "null"
            ],
            "description": "Deep link into the WYDM app relevant to the event, or null/absent when the event has no associated link."
          },
          "dedupe_key": {
            "type": "string",
            "description": "Stable idempotency key for the underlying event. All deliveries (initial + retries) of the same event share this value — dedupe on it."
          }
        }
      },
      "WebhookEnvelope": {
        "type": "object",
        "description": "The signed JSON body WYDM POSTs to a registered webhook endpoint. snake_case. Verify X-Signature (lowercase hex HMAC-SHA256 of the raw body, keyed with the endpoint secret) before trusting it. Typed events add extra fields inside `data`.",
        "required": [
          "id",
          "event",
          "occurred_at",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique id for THIS delivery attempt (the webhookDeliveries row id). A retry of the same event carries a DIFFERENT id — dedupe on `data.dedupe_key`, never on this."
          },
          "event": {
            "type": "string",
            "description": "The event name; identical to the `X-Webhook-Event` header."
          },
          "occurred_at": {
            "type": "number",
            "description": "Unix epoch MILLISECONDS at which the underlying event occurred. Stable across retries of the same event (unlike the per-attempt `X-Webhook-Timestamp` header, which is in seconds)."
          },
          "data": {
            "$ref": "#/components/schemas/WebhookEventData"
          }
        }
      }
    },
    "parameters": {
      "WebhookSignatureHeader": {
        "name": "X-Signature",
        "in": "header",
        "required": true,
        "description": "Lowercase hex HMAC-SHA256 of `X-Webhook-Timestamp + \".\" + rawBody` (the timestamp header value, a literal `.`, then the exact raw request body), keyed with the endpoint's signing secret. Recompute and constant-time compare before trusting the payload.",
        "schema": {
          "type": "string"
        }
      },
      "WebhookTimestampHeader": {
        "name": "X-Webhook-Timestamp",
        "in": "header",
        "required": true,
        "description": "Unix time in SECONDS at which this delivery attempt was signed. Folded into the signed input (`timestamp + \".\" + rawBody`) so it cannot be tampered with. Reject deliveries whose timestamp is too far from your clock to blunt replays. This is the attempt time (fresh per retry), distinct from the envelope's `occurred_at` event time.",
        "schema": {
          "type": "string"
        }
      },
      "WebhookEventHeader": {
        "name": "X-Webhook-Event",
        "in": "header",
        "required": true,
        "description": "The event name (matches the envelope's `event` field).",
        "schema": {
          "type": "string"
        }
      },
      "IdempotencyKeyHeader": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Optional. A unique key (up to 255 characters) that makes this POST idempotent. A retry with the same key and the same request body replays the original response for 24 hours; the same key with a different body returns 409 IDEMPOTENCY_CONFLICT. Keys are scoped per agent key.",
        "schema": {
          "type": "string",
          "maxLength": 255
        }
      }
    }
  }
}
