{
  "openapi": "3.1.0",
  "info": {
    "title": "TAP -- Music PR Software API",
    "version": "2.0.0",
    "description": "API for TAP (Total Audio Promo) — professional music PR software for UK agencies. Contact management, AI enrichment, email validation, pitch drafting, campaign tracking, outcomes, skills, webhooks, and release-brief handoff.\n\nThis document covers the full public surface: 28 versioned endpoints under `/api/v1` plus 6 public (unauthenticated) endpoints.\n\nTAP is the first music PR tool with a public API and MCP server. No other music PR platform offers programmatic access.\n\n**Authentication**: all `/api/v1` endpoints use a workspace API key (`Authorization: Bearer tap_ak_...`) unless noted otherwise — the API-key management endpoint (`/api/v1/keys`) and the release-brief accept/decline endpoints use browser session authentication instead. API keys can only be created on an Agency plan, so key-authenticated access effectively requires an Agency workspace.\n\n**Rate limiting**: key-authenticated endpoints share a limit of 10 requests per minute per API key. Responses include `X-RateLimit-*` headers; exceeding the limit returns 429.\n\n**Human gate**: no API endpoint can send an email or approve a pitch. Agents draft and queue; humans review and send inside the TAP workspace.",
    "contact": {
      "name": "Total Audio Promo",
      "url": "https://totalaudiopromo.com",
      "email": "info@totalaudiopromo.com"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://totalaudiopromo.com",
      "description": "Production"
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "Contacts",
      "description": "Import, list, validate, and enrich music industry contacts"
    },
    {
      "name": "Campaigns",
      "description": "Create, list, update, and manage PR campaigns and their contacts"
    },
    {
      "name": "Pitches",
      "description": "Draft and manage AI-generated pitch emails for campaign contacts"
    },
    {
      "name": "Outcomes",
      "description": "Log and retrieve campaign outcomes (coverage, replies, bounces)"
    },
    {
      "name": "Outreach",
      "description": "Unified read view of the outreach queue (pitch drafts + sequence executions)"
    },
    {
      "name": "Approvals",
      "description": "Read-only view of items awaiting human approval"
    },
    {
      "name": "Actions",
      "description": "Prioritised action queue for agent morning briefs and workspace status"
    },
    {
      "name": "Artists",
      "description": "Workspace artist roster"
    },
    {
      "name": "Skills",
      "description": "Discover, inspect, fork, and run workspace skills; inspect invocation audit rows"
    },
    {
      "name": "Agent Sessions",
      "description": "Poll the status and approval verdict of an agent session"
    },
    {
      "name": "Release Briefs",
      "description": "Inbound release-brief handoff from label partners (e.g. totalaud.io) and accept/decline"
    },
    {
      "name": "Webhooks",
      "description": "Manage webhook subscriptions for workspace events"
    },
    {
      "name": "API Keys",
      "description": "Manage workspace API keys for programmatic access (Agency plan only)"
    },
    {
      "name": "Public",
      "description": "Public endpoints (no authentication required)"
    }
  ],
  "paths": {
    "/api/health": {
      "get": {
        "summary": "Health check",
        "description": "Returns service health status. No authentication required.",
        "operationId": "getHealth",
        "tags": ["Public"],
        "responses": {
          "200": {
            "description": "Service is healthy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "example": "ok" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/newsletter/subscribe": {
      "post": {
        "summary": "Subscribe to newsletter",
        "description": "Subscribe an email address to the TAP fortnightly newsletter. Powered by Resend. No authentication required.",
        "operationId": "subscribeNewsletter",
        "tags": ["Public"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["email"],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "Email address to subscribe"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Successfully subscribed" },
          "400": { "description": "Invalid email address" }
        }
      }
    },
    "/api/share/{token}": {
      "post": {
        "summary": "Access shared report",
        "description": "Access a publicly shared campaign report by its share token. No authentication required.",
        "operationId": "accessSharedReport",
        "tags": ["Public"],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "Share token for the report"
          }
        ],
        "responses": {
          "200": { "description": "Report data returned" },
          "404": { "description": "Share link not found or expired" }
        }
      }
    },
    "/api/share/verify-password": {
      "post": {
        "summary": "Verify share link password",
        "description": "Verify the password for a password-protected shared report. No authentication required.",
        "operationId": "verifySharePassword",
        "tags": ["Public"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["token", "password"],
                "properties": {
                  "token": { "type": "string" },
                  "password": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Password verified, report data returned" },
          "401": { "description": "Invalid password" }
        }
      }
    },
    "/api/coverage/reports/{token}": {
      "get": {
        "summary": "Get public coverage report",
        "description": "Retrieve a publicly shared coverage report by its share token. No authentication required.",
        "operationId": "getCoverageReport",
        "tags": ["Public"],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "Share token for the coverage report"
          }
        ],
        "responses": {
          "200": { "description": "Coverage report data" },
          "404": { "description": "Report not found" }
        }
      }
    },
    "/api/coverage/reports/verify-password": {
      "post": {
        "summary": "Verify coverage report password",
        "description": "Verify the password for a password-protected coverage report. No authentication required.",
        "operationId": "verifyCoverageReportPassword",
        "tags": ["Public"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["token", "password"],
                "properties": {
                  "token": { "type": "string" },
                  "password": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Password verified" },
          "401": { "description": "Invalid password" }
        }
      }
    },
    "/api/v1/contacts": {
      "get": {
        "summary": "List contacts",
        "description": "List contacts for the authenticated workspace with pagination and filtering. Required scope: `contacts:read`.",
        "operationId": "listContacts",
        "tags": ["Contacts"],
        "security": [{ "apiKey": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/Page" },
          { "$ref": "#/components/parameters/Limit" },
          {
            "name": "search",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Partial match on name, email, or outlet"
          },
          {
            "name": "platform_type",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Filter by source platform type"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of contacts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "contacts": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Contact" }
                    },
                    "total": { "type": "integer", "description": "Total matching contacts" },
                    "page": { "type": "integer" },
                    "limit": { "type": "integer" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "summary": "Import contacts",
        "description": "Import contacts into the authenticated workspace. Supports batch import with email validation, deduplication, and idempotency. Maximum 500 contacts per request. Required scope: `contacts:write`.",
        "operationId": "importContacts",
        "tags": ["Contacts"],
        "security": [{ "apiKey": [] }],
        "parameters": [
          {
            "name": "dry_run",
            "in": "query",
            "schema": { "type": "string", "enum": ["true"] },
            "description": "Validate and dedup without inserting"
          },
          {
            "name": "X-Idempotency-Key",
            "in": "header",
            "schema": { "type": "string" },
            "description": "Optional idempotency key to prevent duplicate processing on retry (1-hour TTL)"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["contacts"],
                "properties": {
                  "contacts": {
                    "type": "array",
                    "maxItems": 500,
                    "items": { "$ref": "#/components/schemas/ContactImport" }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Import results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "imported": {
                      "type": "integer",
                      "description": "Number of contacts successfully imported"
                    },
                    "skipped": {
                      "type": "integer",
                      "description": "Number of duplicates skipped (already in workspace)"
                    },
                    "invalid": {
                      "type": "integer",
                      "description": "Number of invalid entries (e.g. missing email)"
                    },
                    "duplicatesInRequest": {
                      "type": "integer",
                      "description": "Number of within-request duplicates merged"
                    },
                    "dry_run": {
                      "type": "boolean",
                      "description": "Present and true if dry_run was used"
                    },
                    "results": {
                      "type": "array",
                      "maxItems": 100,
                      "items": {
                        "type": "object",
                        "properties": {
                          "email": { "type": "string" },
                          "status": {
                            "type": "string",
                            "enum": ["imported", "skipped", "invalid", "duplicate_in_request"]
                          },
                          "reason": { "type": "string" },
                          "name": { "type": "string" }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid request body or contacts array empty/too large" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/contacts/{id}": {
      "get": {
        "summary": "Get contact",
        "description": "Retrieve a single contact with all enrichment data. Required scope: `contacts:read`.",
        "operationId": "getContact",
        "tags": ["Contacts"],
        "security": [{ "apiKey": [] }],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" },
            "description": "Contact ID"
          }
        ],
        "responses": {
          "200": {
            "description": "Contact retrieved (full row including enrichment fields)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "contact": { "$ref": "#/components/schemas/Contact" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Contact not found" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "delete": {
        "summary": "Erase contact (GDPR)",
        "description": "GDPR erasure — anonymises the contact's personal data rather than hard-deleting the row. Emits a `contact_removed` signal. Required scope: `contacts:write`.",
        "operationId": "eraseContact",
        "tags": ["Contacts"],
        "security": [{ "apiKey": [] }],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" },
            "description": "Contact ID"
          }
        ],
        "responses": {
          "200": {
            "description": "Contact erased",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "erased": { "type": "boolean", "example": true },
                    "anonymisedFields": {
                      "type": "array",
                      "items": { "type": "string" },
                      "description": "Names of the fields that were anonymised"
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Contact not found" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "description": "Erasure request failed" }
        }
      }
    },
    "/api/v1/contacts/validate": {
      "post": {
        "summary": "Validate email addresses",
        "description": "Validate a batch of email addresses. Returns validation results including confidence level, SMTP verification, catch-all detection, role-based and disposable email detection. Maximum 100 emails per request. Required scope: `validate`.",
        "operationId": "validateEmails",
        "tags": ["Contacts"],
        "security": [{ "apiKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["emails"],
                "properties": {
                  "emails": {
                    "type": "array",
                    "maxItems": 100,
                    "items": { "type": "string", "format": "email" }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Validation results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/ValidationResult" }
                    }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid request or emails array empty/too large" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/contacts/enrich": {
      "post": {
        "summary": "Enrich contacts with AI",
        "description": "Enrich contacts with AI-powered intelligence using Claude. Adds role detail, genres, coverage area, contact method, best timing, submission guidelines, pitch tips, BBC station detection, and geographic scope. Monthly limits enforced per subscription tier: Free (10), Pro (200), Agency (5000). Maximum 50 contacts per request. Required scope: `enrich`.",
        "operationId": "enrichContacts",
        "tags": ["Contacts"],
        "security": [{ "apiKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["contactIds"],
                "properties": {
                  "contactIds": {
                    "type": "array",
                    "maxItems": 50,
                    "items": { "type": "string", "format": "uuid" },
                    "description": "Contact IDs to enrich (must belong to the authenticated workspace)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Enrichment results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total": {
                      "type": "integer",
                      "description": "Number of contacts processed"
                    },
                    "enriched": { "type": "integer", "description": "Successfully enriched" },
                    "failed": { "type": "integer", "description": "Failed enrichments" },
                    "creditsUsed": { "type": "integer" },
                    "creditsRemaining": {
                      "type": "integer",
                      "description": "Remaining monthly enrichment credits"
                    },
                    "results": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/EnrichmentResult" }
                    }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid request or contactIds array empty/too large" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "No contacts found for the provided IDs" },
          "429": {
            "description": "Monthly enrichment limit reached for current subscription tier, or rate limit exceeded"
          },
          "503": { "description": "AI enrichment not configured" }
        }
      }
    },
    "/api/v1/shared-contacts": {
      "get": {
        "summary": "List shared contact seed",
        "description": "List rows from the shared (cross-workspace) journalist and radio contact seed. Read-only reference data. Required scope: `contacts:read`.",
        "operationId": "listSharedContacts",
        "tags": ["Contacts"],
        "security": [{ "apiKey": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/Page" },
          { "$ref": "#/components/parameters/Limit" },
          {
            "name": "outlet",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Partial match on outlet name"
          },
          {
            "name": "country",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Exact match on country (stored verbatim, ISO-2 or human-readable)"
          },
          {
            "name": "genre",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Single genre tag — matches when present in the genre_tags array"
          },
          {
            "name": "search",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Partial match across name, outlet, and role"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of shared contacts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "contacts": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/SharedContact" }
                    },
                    "total": { "type": "integer" },
                    "page": { "type": "integer" },
                    "limit": { "type": "integer" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/campaigns": {
      "get": {
        "summary": "List campaigns",
        "description": "List campaigns for the authenticated workspace with pagination and filtering by status or artist name. Required scope: `campaigns:read`.",
        "operationId": "listCampaigns",
        "tags": ["Campaigns"],
        "security": [{ "apiKey": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/Page" },
          { "$ref": "#/components/parameters/Limit" },
          {
            "name": "status",
            "in": "query",
            "schema": { "type": "string", "enum": ["active", "draft", "paused", "completed"] },
            "description": "Filter by campaign status"
          },
          {
            "name": "artist_name",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Partial match on artist name"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of campaigns",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "campaigns": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/CampaignSummary" }
                    },
                    "total": { "type": "integer" },
                    "page": { "type": "integer" },
                    "limit": { "type": "integer" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "summary": "Create campaign",
        "description": "Create a new PR campaign in the authenticated workspace. Required scope: `campaigns:write`.",
        "operationId": "createCampaign",
        "tags": ["Campaigns"],
        "security": [{ "apiKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name"],
                "properties": {
                  "name": { "type": "string", "description": "Campaign name" },
                  "artist_name": { "type": "string", "description": "Artist name" },
                  "release_title": { "type": "string", "description": "Release title" },
                  "release_date": {
                    "type": "string",
                    "format": "date",
                    "description": "Release date (YYYY-MM-DD)"
                  },
                  "services": {
                    "type": "array",
                    "items": { "type": "string" },
                    "description": "Services for this campaign (e.g. radio, press, playlist)"
                  },
                  "status": {
                    "type": "string",
                    "enum": ["active", "draft", "paused", "completed"],
                    "default": "draft",
                    "description": "Campaign status"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Campaign created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "campaign": { "$ref": "#/components/schemas/CampaignSummary" }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid request (missing name)" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/campaigns/{id}": {
      "get": {
        "summary": "Get campaign",
        "description": "Retrieve a single campaign with full details, contact stats, and recent activity. Required scope: `campaigns:read`.",
        "operationId": "getCampaign",
        "tags": ["Campaigns"],
        "security": [{ "apiKey": [] }],
        "parameters": [{ "$ref": "#/components/parameters/CampaignId" }],
        "responses": {
          "200": {
            "description": "Campaign details with stats and recent activity",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "campaign": { "$ref": "#/components/schemas/CampaignDetail" },
                    "stats": { "$ref": "#/components/schemas/CampaignStats" },
                    "recent_activity": {
                      "type": "array",
                      "maxItems": 5,
                      "items": { "$ref": "#/components/schemas/ActivityEntry" }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Campaign not found" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "patch": {
        "summary": "Update campaign",
        "description": "Partial update of campaign fields. Only provided fields are updated. Required scope: `campaigns:write`.",
        "operationId": "updateCampaign",
        "tags": ["Campaigns"],
        "security": [{ "apiKey": [] }],
        "parameters": [{ "$ref": "#/components/parameters/CampaignId" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": { "type": "string" },
                  "artist_name": { "type": "string" },
                  "release_title": { "type": "string" },
                  "release_date": { "type": "string", "format": "date" },
                  "status": {
                    "type": "string",
                    "enum": ["active", "draft", "paused", "completed"]
                  },
                  "goal": { "type": "string" },
                  "services": { "type": "array", "items": { "type": "string" } },
                  "embargo_date": { "type": "string", "format": "date" },
                  "embargo_notes": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Campaign updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "campaign": { "$ref": "#/components/schemas/CampaignDetail" }
                  }
                }
              }
            }
          },
          "400": { "description": "No valid fields provided" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Campaign not found" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "delete": {
        "summary": "Delete campaign",
        "description": "Remove a campaign and clean up its Google Calendar blocks. Required scope: `campaigns:write`.",
        "operationId": "deleteCampaign",
        "tags": ["Campaigns"],
        "security": [{ "apiKey": [] }],
        "parameters": [{ "$ref": "#/components/parameters/CampaignId" }],
        "responses": {
          "200": {
            "description": "Campaign deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": { "type": "boolean", "example": true },
                    "id": { "type": "string", "format": "uuid" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Campaign not found" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/campaigns/{id}/contacts": {
      "get": {
        "summary": "List campaign contacts",
        "description": "List contacts linked to a campaign with their pitch status and engagement state. Required scope: `campaigns:read`.",
        "operationId": "listCampaignContacts",
        "tags": ["Campaigns"],
        "security": [{ "apiKey": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/CampaignId" },
          { "$ref": "#/components/parameters/Page" },
          { "$ref": "#/components/parameters/Limit" }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of campaign contacts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "contacts": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/CampaignContactEntry" }
                    },
                    "total": { "type": "integer" },
                    "page": { "type": "integer" },
                    "limit": { "type": "integer" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Campaign not found" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "summary": "Add contacts to campaign",
        "description": "Link existing contacts to a campaign by their contact IDs. Maximum 200 contacts per request. Required scope: `campaigns:write`.",
        "operationId": "addCampaignContacts",
        "tags": ["Campaigns"],
        "security": [{ "apiKey": [] }],
        "parameters": [{ "$ref": "#/components/parameters/CampaignId" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["contact_ids"],
                "properties": {
                  "contact_ids": {
                    "type": "array",
                    "maxItems": 200,
                    "items": { "type": "string", "format": "uuid" },
                    "description": "Contact IDs to add to the campaign (must belong to the workspace)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Contacts added",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "added": {
                      "type": "integer",
                      "description": "Number of contacts newly linked"
                    },
                    "already_linked": {
                      "type": "integer",
                      "description": "Number already in this campaign"
                    },
                    "invalid_ids": {
                      "type": "array",
                      "items": { "type": "string" },
                      "description": "Contact IDs not found in this workspace"
                    }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid request or contact_ids array empty/too large" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Campaign not found or no valid contacts found" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/campaigns/{id}/pitches": {
      "get": {
        "summary": "List pitch drafts",
        "description": "List all pitch drafts for a campaign. Returns AI-generated and manually created drafts with their metadata. Required scope: `pitches:read`.",
        "operationId": "listCampaignPitches",
        "tags": ["Pitches"],
        "security": [{ "apiKey": [] }],
        "parameters": [{ "$ref": "#/components/parameters/CampaignId" }],
        "responses": {
          "200": {
            "description": "Pitch drafts retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "pitches": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "string" },
                          "title": { "type": "string" },
                          "subject": { "type": "string" },
                          "body": { "type": "string" },
                          "source": { "type": "string", "enum": ["manual", "generated"] },
                          "voice_profile_id": { "type": "string", "nullable": true },
                          "send_status": { "type": "string" },
                          "last_used_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "successful_outcomes_count": { "type": "integer" },
                          "created_at": { "type": "string", "format": "date-time" },
                          "updated_at": { "type": "string", "format": "date-time" }
                        }
                      }
                    },
                    "total": { "type": "integer" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Campaign not found" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "summary": "Generate pitch drafts",
        "description": "Generate AI pitch drafts for contacts in a campaign. This is the 'queue for review' pattern -- drafts are created, NOT sent. Agents draft, humans send. Maximum 20 contacts per request. Required scope: `pitches:write`.",
        "operationId": "generateCampaignPitches",
        "tags": ["Pitches"],
        "security": [{ "apiKey": [] }],
        "parameters": [{ "$ref": "#/components/parameters/CampaignId" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["contact_ids"],
                "properties": {
                  "contact_ids": {
                    "type": "array",
                    "items": { "type": "string", "format": "uuid" },
                    "maxItems": 20,
                    "description": "Contact IDs to generate pitches for (must be linked to this campaign)"
                  },
                  "variant_count": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 5,
                    "default": 3,
                    "description": "Number of tone variants to generate per contact"
                  },
                  "voice_profile_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Optional voice profile to use for pitch generation"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Pitch drafts generated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "drafts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "contact_id": { "type": "string" },
                          "contact_name": { "type": "string" },
                          "variants": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "subject": { "type": "string" },
                                "body": { "type": "string" },
                                "tone": {
                                  "type": "string",
                                  "enum": ["professional", "casual", "enthusiastic"]
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "errors": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "contact_id": { "type": "string" },
                          "error": { "type": "string" }
                        }
                      }
                    },
                    "summary": {
                      "type": "object",
                      "properties": {
                        "requested": { "type": "integer" },
                        "generated": { "type": "integer" },
                        "failed": { "type": "integer" },
                        "invalid": { "type": "integer" }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid request body or contacts not linked to campaign" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Campaign not found" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/campaigns/{id}/outcomes": {
      "get": {
        "summary": "List campaign outcomes",
        "description": "List all outcomes for a campaign with contact details, pagination, and chronological ordering. Required scope: `outcomes:read`.",
        "operationId": "listCampaignOutcomes",
        "tags": ["Outcomes"],
        "security": [{ "apiKey": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/CampaignId" },
          { "$ref": "#/components/parameters/Page" },
          { "$ref": "#/components/parameters/Limit" }
        ],
        "responses": {
          "200": {
            "description": "Outcomes retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "outcomes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "string" },
                          "contact_id": { "type": "string" },
                          "contact_name": { "type": "string", "nullable": true },
                          "contact_outlet": { "type": "string", "nullable": true },
                          "outcome_type": {
                            "type": "string",
                            "enum": [
                              "reply",
                              "coverage",
                              "bounce",
                              "no_response",
                              "added_to_rotation"
                            ]
                          },
                          "channel": { "type": "string", "nullable": true },
                          "notes": { "type": "string", "nullable": true },
                          "coverage_url": { "type": "string", "nullable": true },
                          "logged_at": { "type": "string", "format": "date-time" },
                          "created_at": { "type": "string", "format": "date-time" }
                        }
                      }
                    },
                    "total": { "type": "integer" },
                    "page": { "type": "integer" },
                    "limit": { "type": "integer" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Campaign not found" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "summary": "Log campaign outcome",
        "description": "Log a campaign outcome for a contact. Updates the campaign_contacts record and emits a tap_signal. Valid outcome types: reply, coverage, bounce, no_response, added_to_rotation. Required scope: `outcomes:write`.",
        "operationId": "logCampaignOutcome",
        "tags": ["Outcomes"],
        "security": [{ "apiKey": [] }],
        "parameters": [{ "$ref": "#/components/parameters/CampaignId" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["contact_id", "outcome_type"],
                "properties": {
                  "contact_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Contact ID (must be linked to this campaign)"
                  },
                  "outcome_type": {
                    "type": "string",
                    "enum": ["reply", "coverage", "bounce", "no_response", "added_to_rotation"],
                    "description": "Type of outcome to log"
                  },
                  "notes": {
                    "type": "string",
                    "description": "Optional notes about the outcome"
                  },
                  "coverage_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "URL of coverage article or evidence"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Outcome logged",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "outcome": {
                      "type": "object",
                      "properties": {
                        "id": { "type": "string" },
                        "contact_id": { "type": "string" },
                        "contact_name": { "type": "string", "nullable": true },
                        "outcome_type": { "type": "string" },
                        "notes": { "type": "string", "nullable": true },
                        "coverage_url": { "type": "string", "nullable": true },
                        "logged_at": { "type": "string", "format": "date-time" },
                        "created_at": { "type": "string", "format": "date-time" }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid request body or contact not linked to campaign" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Campaign, contact, or campaign-contact link not found" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/outcomes": {
      "get": {
        "summary": "List workspace outcomes",
        "description": "List outcomes across the whole workspace, filterable by campaign, contact, or outcome type. Note this endpoint uses the workspace-level outcome vocabulary (see enum), which differs from the per-campaign endpoint. Required scope: `campaigns:read`.",
        "operationId": "listOutcomes",
        "tags": ["Outcomes"],
        "security": [{ "apiKey": [] }],
        "parameters": [
          {
            "name": "campaign_id",
            "in": "query",
            "schema": { "type": "string", "format": "uuid" },
            "description": "Filter by campaign"
          },
          {
            "name": "contact_id",
            "in": "query",
            "schema": { "type": "string", "format": "uuid" },
            "description": "Filter by contact"
          },
          {
            "name": "outcome_type",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "no_response",
                "opened",
                "replied_positive",
                "replied_negative",
                "added_to_playlist",
                "interview_booked",
                "coverage",
                "declined",
                "other"
              ]
            },
            "description": "Filter by outcome type"
          },
          { "$ref": "#/components/parameters/Page" },
          { "$ref": "#/components/parameters/Limit" }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of outcomes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "outcomes": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/WorkspaceOutcome" }
                    },
                    "total": { "type": "integer" },
                    "page": { "type": "integer" },
                    "limit": { "type": "integer" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "summary": "Log workspace outcome",
        "description": "Log an outcome for a campaign contact. Dispatches an `outcome.logged` webhook event (fire-and-forget). Required scope: `campaigns:write`.",
        "operationId": "logOutcome",
        "tags": ["Outcomes"],
        "security": [{ "apiKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["campaign_id", "contact_id", "outcome_type"],
                "properties": {
                  "campaign_id": { "type": "string", "format": "uuid" },
                  "contact_id": { "type": "string", "format": "uuid" },
                  "outcome_type": {
                    "type": "string",
                    "enum": [
                      "no_response",
                      "opened",
                      "replied_positive",
                      "replied_negative",
                      "added_to_playlist",
                      "interview_booked",
                      "coverage",
                      "declined",
                      "other"
                    ]
                  },
                  "channel": {
                    "type": "string",
                    "default": "email",
                    "description": "Channel the outcome occurred on"
                  },
                  "notes": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Outcome logged",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/WorkspaceOutcome" }
              }
            }
          },
          "400": { "description": "Missing required fields or invalid outcome_type" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Campaign not found in workspace" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/outcomes/{id}": {
      "get": {
        "summary": "Get outcome",
        "description": "Retrieve a single outcome. Required scope: `campaigns:read`.",
        "operationId": "getOutcome",
        "tags": ["Outcomes"],
        "security": [{ "apiKey": [] }],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" },
            "description": "Outcome ID"
          }
        ],
        "responses": {
          "200": {
            "description": "Outcome retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "outcome": { "$ref": "#/components/schemas/WorkspaceOutcome" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Outcome not found" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "patch": {
        "summary": "Update outcome",
        "description": "Correct or annotate a previously logged outcome. Updatable fields: outcome_type, notes (max 1000 characters), channel. Required scope: `campaigns:write`.",
        "operationId": "updateOutcome",
        "tags": ["Outcomes"],
        "security": [{ "apiKey": [] }],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" },
            "description": "Outcome ID"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "outcome_type": {
                    "type": "string",
                    "enum": [
                      "no_response",
                      "opened",
                      "replied_positive",
                      "replied_negative",
                      "added_to_playlist",
                      "interview_booked",
                      "coverage",
                      "declined",
                      "other"
                    ]
                  },
                  "notes": { "type": "string", "maxLength": 1000, "nullable": true },
                  "channel": { "type": "string", "nullable": true }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Outcome updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "outcome": { "$ref": "#/components/schemas/WorkspaceOutcome" }
                  }
                }
              }
            }
          },
          "400": { "description": "No updatable fields provided or invalid values" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Outcome not found or update failed" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/outreach/queue": {
      "get": {
        "summary": "Get outreach queue",
        "description": "Unified read view across pitch drafts and sequence executions. Each row represents one outreach attempt for a contact on a campaign, on a channel, in a state, after N dispatch attempts. Read-only — writes go to the underlying surfaces (pitch drafts via `/api/v1/campaigns/{id}/pitches`; sequence executions via the internal pipeline). Required scope: `pitches:read`.",
        "operationId": "getOutreachQueue",
        "tags": ["Outreach"],
        "security": [{ "apiKey": [] }],
        "parameters": [
          {
            "name": "campaign_id",
            "in": "query",
            "schema": { "type": "string", "format": "uuid" },
            "description": "Filter to a single campaign"
          },
          {
            "name": "channel",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": ["email", "portal", "instagram", "linkedin", "physical_post"]
            },
            "description": "Filter by outreach channel"
          },
          {
            "name": "source",
            "in": "query",
            "schema": { "type": "string", "enum": ["pitch_draft", "sequence_execution"] },
            "description": "Filter by row source"
          },
          {
            "name": "state",
            "in": "query",
            "schema": { "type": "string", "pattern": "^[a-z_]{1,32}$" },
            "description": "Approval/sequence state (e.g. pending, approved, pending_approval, auto)"
          },
          { "$ref": "#/components/parameters/Page" },
          { "$ref": "#/components/parameters/Limit" }
        ],
        "responses": {
          "200": {
            "description": "Paginated outreach queue",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/OutreachQueueItem" }
                    },
                    "total": { "type": "integer" },
                    "page": { "type": "integer" },
                    "limit": { "type": "integer" }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid channel, source, or state filter" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Campaign not found in this workspace" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/approvals/queue": {
      "get": {
        "summary": "Get approvals queue",
        "description": "Read-only view of the workspace's pending-approval items — pitch drafts awaiting human review across every campaign. Deliberately read-only: approving or sending a pitch is a per-message human action inside the TAP workspace. No API caller can flip a pitch to approved or sent. Required scope: `approvals:read`.",
        "operationId": "getApprovalsQueue",
        "tags": ["Approvals"],
        "security": [{ "apiKey": [] }],
        "parameters": [
          {
            "name": "campaign_id",
            "in": "query",
            "schema": { "type": "string", "format": "uuid" },
            "description": "Filter to a single campaign"
          },
          { "$ref": "#/components/parameters/Page" },
          { "$ref": "#/components/parameters/Limit" }
        ],
        "responses": {
          "200": {
            "description": "Pending approval items",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/ApprovalQueueItem" }
                    },
                    "total": { "type": "integer" },
                    "page": { "type": "integer" },
                    "limit": { "type": "integer" },
                    "note": {
                      "type": "string",
                      "description": "Reminder that approve and send happen inside the TAP workspace"
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Campaign not found in this workspace" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/actions": {
      "get": {
        "summary": "Get action queue",
        "description": "Get the prioritised action queue for the workspace. Returns the same data as the home page ActionQueue component. This is what powers the 'morning brief' for agents. Action types: overdue_followup, stalled_campaign, unread_reply, contact_going_cold, pending_coverage. Required scope: `actions:read`.",
        "operationId": "getActionQueue",
        "tags": ["Actions"],
        "security": [{ "apiKey": [] }],
        "responses": {
          "200": {
            "description": "Action queue retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "actions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "type": {
                            "type": "string",
                            "enum": [
                              "overdue_followup",
                              "stalled_campaign",
                              "unread_reply",
                              "contact_going_cold",
                              "pending_coverage"
                            ],
                            "description": "Action type"
                          },
                          "priority": {
                            "type": "string",
                            "enum": ["urgent", "attention", "info"],
                            "description": "Priority level"
                          },
                          "entity_name": {
                            "type": "string",
                            "description": "Human-readable name of the entity (contact, campaign, etc.)"
                          },
                          "entity_id": {
                            "type": "string",
                            "description": "ID of the related entity"
                          },
                          "reason": {
                            "type": "string",
                            "description": "Human-readable reason for the action"
                          },
                          "action_url": {
                            "type": "string",
                            "description": "Relative URL to take action in the TAP workspace"
                          }
                        }
                      }
                    },
                    "total": { "type": "integer" },
                    "generated_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "Timestamp when the action queue was generated"
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/artists": {
      "get": {
        "summary": "List artists",
        "description": "List artists in the workspace roster, ordered by name. Archived artists are excluded unless requested. Required scope: `artists:read`.",
        "operationId": "listArtists",
        "tags": ["Artists"],
        "security": [{ "apiKey": [] }],
        "parameters": [
          {
            "name": "include_archived",
            "in": "query",
            "schema": { "type": "string", "enum": ["true"] },
            "description": "Include archived artists in the response"
          }
        ],
        "responses": {
          "200": {
            "description": "Artist list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "artists": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Artist" }
                    },
                    "total": { "type": "integer" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "summary": "Create artist",
        "description": "Create a new artist in the workspace roster. Required scope: `artists:write`.",
        "operationId": "createArtist",
        "tags": ["Artists"],
        "security": [{ "apiKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name"],
                "properties": {
                  "name": { "type": "string", "maxLength": 120, "description": "Artist name" },
                  "genre": { "type": "string", "maxLength": 120 },
                  "bio": { "type": "string", "maxLength": 2000 },
                  "spotify_url": { "type": "string", "maxLength": 500 },
                  "apple_music_url": { "type": "string", "maxLength": 500 },
                  "website": { "type": "string", "maxLength": 500 },
                  "instagram": { "type": "string", "maxLength": 120 },
                  "twitter": { "type": "string", "maxLength": 120 }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Artist created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "artist": { "$ref": "#/components/schemas/Artist" }
                  }
                }
              }
            }
          },
          "400": { "description": "Missing or invalid fields" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/skills": {
      "get": {
        "summary": "List skills",
        "description": "List every skill visible to the authenticated workspace — TAP defaults overlaid with any workspace bindings that have edits. Use this to discover what the workspace can do programmatically via `POST /api/v1/skills/{slug}/run`. Required scope: `skills:read`.",
        "operationId": "listSkills",
        "tags": ["Skills"],
        "security": [{ "apiKey": [] }],
        "responses": {
          "200": {
            "description": "Skill list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "skills": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/SkillSummary" }
                    },
                    "total": { "type": "integer" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/skills/{slug}": {
      "get": {
        "summary": "Get skill",
        "description": "Return the effective skill for this workspace — the active manifest plus any workspace body override. Useful for fetching skill markdown before invoking. Required scope: `skills:read`.",
        "operationId": "getSkill",
        "tags": ["Skills"],
        "security": [{ "apiKey": [] }],
        "parameters": [{ "$ref": "#/components/parameters/SkillSlug" }],
        "responses": {
          "200": {
            "description": "Skill detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "skill": { "$ref": "#/components/schemas/SkillDetail" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Skill not found" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/skills/{slug}/fork": {
      "post": {
        "summary": "Fork skill",
        "description": "Create or update a workspace-owned override for a default skill. If `body` is null, the override is removed and the manifest default is restored. Required scope: `skills:write`.",
        "operationId": "forkSkill",
        "tags": ["Skills"],
        "security": [{ "apiKey": [] }],
        "parameters": [{ "$ref": "#/components/parameters/SkillSlug" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["body"],
                "properties": {
                  "body": {
                    "type": ["string", "null"],
                    "description": "New skill body markdown, or null to reset to the default"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Override created, updated, or removed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "slug": { "type": "string" },
                    "source": { "type": "string", "enum": ["workspace", "default"] },
                    "body_length": { "type": "integer" }
                  }
                }
              }
            }
          },
          "400": { "description": "body is required (string to override, null to reset)" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Skill not found" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/skills/{slug}/run": {
      "post": {
        "summary": "Run skill",
        "description": "Execute a workspace skill. Writes an audit row to the invocation log regardless of outcome. Skill handlers never bypass the approval queue — the per-message human gate is preserved. Returns 200 when the skill succeeds and 422 when the skill run fails (both carry the same response shape). Required scope: `skills:write`.",
        "operationId": "runSkill",
        "tags": ["Skills"],
        "security": [{ "apiKey": [] }],
        "parameters": [{ "$ref": "#/components/parameters/SkillSlug" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "input": {
                    "type": "object",
                    "description": "Skill input payload — shape depends on the skill's declared io"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Skill run completed successfully",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SkillRunResult" }
              }
            }
          },
          "422": {
            "description": "Skill run failed (see error field)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SkillRunResult" }
              }
            }
          },
          "400": { "description": "Invalid JSON body" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/invocations/{id}": {
      "get": {
        "summary": "Get skill invocation",
        "description": "Fetch a single skill invocation audit row, scoped to the workspace that issued the API key. Drives the MCP `tap_get_invocation` tool and the Co-pilot History tab. Required scope: `skills:read`.",
        "operationId": "getInvocation",
        "tags": ["Skills"],
        "security": [{ "apiKey": [] }],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" },
            "description": "Invocation ID"
          }
        ],
        "responses": {
          "200": {
            "description": "Invocation retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "invocation": {
                      "type": "object",
                      "properties": {
                        "id": { "type": "string", "format": "uuid" },
                        "skill_slug": { "type": "string" },
                        "version": { "type": "string" },
                        "inputs": { "type": "object" },
                        "outputs": { "type": "object", "nullable": true },
                        "error": { "type": "string", "nullable": true },
                        "duration_ms": { "type": "integer" },
                        "tokens_used": { "type": "integer", "nullable": true },
                        "confidence": { "type": "number", "nullable": true },
                        "status": { "type": "string", "enum": ["completed", "failed"] },
                        "created_at": { "type": "string", "format": "date-time" }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Invocation not found" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/agent-sessions/{sessionId}": {
      "get": {
        "summary": "Get agent session",
        "description": "Returns the current status of an agent session, including any pending approval. Used by external agents to poll the verdict on an approval request, or to inspect what an in-progress agent is currently doing. For production use, prefer the agent.approval_granted / agent.approval_rejected webhook events instead of polling. Required scope: `agents:read`.",
        "operationId": "getAgentSession",
        "tags": ["Agent Sessions"],
        "security": [{ "apiKey": [] }],
        "parameters": [
          {
            "name": "sessionId",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "Agent session ID"
          }
        ],
        "responses": {
          "200": {
            "description": "Session status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "session": {
                      "type": "object",
                      "properties": {
                        "session_id": { "type": "string" },
                        "status": {
                          "type": "string",
                          "description": "Raw session status (e.g. awaiting_approval, running, completed, cancelled)"
                        },
                        "verdict": {
                          "type": "string",
                          "description": "Derived approval verdict: pending, approved, rejected, or the raw status"
                        },
                        "agent_type": { "type": "string" },
                        "pending_approval": {
                          "type": "object",
                          "nullable": true,
                          "description": "Details of the approval currently awaited, if any"
                        },
                        "started_at": { "type": "string", "format": "date-time" },
                        "completed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "error": { "type": "string", "nullable": true }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Session not found" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/release-briefs": {
      "post": {
        "summary": "Submit release brief",
        "description": "Receive an inbound release brief from a label partner (e.g. totalaud.io). Idempotent on `source_brief_id`: returns 201 on first receipt, 200 if the same brief has already been submitted to this workspace (no second row created). The brief lands in the TAP inbox for a human to accept or decline. Required scope: `briefs:write`.",
        "operationId": "submitReleaseBrief",
        "tags": ["Release Briefs"],
        "security": [{ "apiKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ReleaseBriefPayload" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Brief received",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ReleaseBriefReceipt" }
              }
            }
          },
          "200": {
            "description": "Brief already received (idempotent replay) — original row returned",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ReleaseBriefReceipt" }
              }
            }
          },
          "400": { "description": "Invalid JSON body" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": {
            "description": "Validation failed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean", "example": false },
                    "error": { "type": "string", "example": "Validation failed" },
                    "issues": {
                      "type": "array",
                      "items": { "type": "object" },
                      "description": "Field-level validation issues"
                    }
                  }
                }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/release-briefs/{id}/accept": {
      "post": {
        "summary": "Accept release brief",
        "description": "Accept a pending release brief — creates a campaign from the brief payload and marks the brief accepted. Requires session authentication (workspace member), not an API key.",
        "operationId": "acceptReleaseBrief",
        "tags": ["Release Briefs"],
        "security": [{ "session": [] }],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" },
            "description": "Inbox release brief ID"
          }
        ],
        "responses": {
          "200": {
            "description": "Brief accepted, campaign created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "campaign_id": { "type": "string", "format": "uuid" },
                    "campaign_name": { "type": "string" },
                    "redirect_path": { "type": "string", "example": "/campaigns/{id}" }
                  }
                }
              }
            }
          },
          "401": { "description": "Not authenticated" },
          "403": { "description": "Not a member of the brief's workspace" },
          "404": { "description": "Brief not found" },
          "409": { "description": "Brief is already accepted or declined" },
          "500": { "description": "Failed to create campaign" }
        }
      }
    },
    "/api/v1/release-briefs/{id}/decline": {
      "post": {
        "summary": "Decline release brief",
        "description": "Decline a pending release brief with an optional reason. Requires session authentication (workspace member), not an API key.",
        "operationId": "declineReleaseBrief",
        "tags": ["Release Briefs"],
        "security": [{ "session": [] }],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" },
            "description": "Inbox release brief ID"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "Optional decline reason"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Brief declined",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "example": "declined" }
                  }
                }
              }
            }
          },
          "401": { "description": "Not authenticated" },
          "403": { "description": "Not a member of the brief's workspace" },
          "404": { "description": "Brief not found" },
          "409": { "description": "Brief is already accepted or declined" }
        }
      }
    },
    "/api/v1/webhooks": {
      "get": {
        "summary": "List webhook subscriptions",
        "description": "List all webhook subscriptions for the authenticated workspace. Secrets are never returned. Required scope: `webhooks:read`.",
        "operationId": "listWebhooks",
        "tags": ["Webhooks"],
        "security": [{ "apiKey": [] }],
        "responses": {
          "200": {
            "description": "Webhook subscriptions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhooks": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/WebhookSubscription" }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "summary": "Create webhook subscription",
        "description": "Create a new webhook subscription. The URL must use HTTPS. If no secret (minimum 16 characters) is supplied, a `whsec_...` secret is generated and returned once in the response — it cannot be recovered afterwards. Required scope: `webhooks:write`.",
        "operationId": "createWebhook",
        "tags": ["Webhooks"],
        "security": [{ "apiKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["url", "events"],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "HTTPS destination URL"
                  },
                  "events": {
                    "type": "array",
                    "minItems": 1,
                    "items": { "$ref": "#/components/schemas/WebhookEvent" },
                    "description": "Events to subscribe to"
                  },
                  "secret": {
                    "type": "string",
                    "minLength": 16,
                    "description": "Optional signing secret (minimum 16 characters). Auto-generated if omitted."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Subscription created (secret shown once)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhook": {
                      "allOf": [
                        { "$ref": "#/components/schemas/WebhookSubscription" },
                        {
                          "type": "object",
                          "properties": {
                            "secret": {
                              "type": "string",
                              "description": "Signing secret — returned once on creation only"
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid URL (must be HTTPS) or invalid events array" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/webhooks/{id}": {
      "get": {
        "summary": "Get webhook subscription",
        "description": "Fetch a single webhook subscription's metadata. The secret is NOT returned — it is shown once on creation. Required scope: `webhooks:read`.",
        "operationId": "getWebhook",
        "tags": ["Webhooks"],
        "security": [{ "apiKey": [] }],
        "parameters": [{ "$ref": "#/components/parameters/WebhookId" }],
        "responses": {
          "200": {
            "description": "Subscription retrieved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhook": { "$ref": "#/components/schemas/WebhookSubscription" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Webhook subscription not found" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "patch": {
        "summary": "Update webhook subscription",
        "description": "Update a webhook subscription. Allows toggling `active` and replacing the `events` array. URL and secret are immutable — create a new subscription if the destination or signing key changes. Required scope: `webhooks:write`.",
        "operationId": "updateWebhook",
        "tags": ["Webhooks"],
        "security": [{ "apiKey": [] }],
        "parameters": [{ "$ref": "#/components/parameters/WebhookId" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "active": { "type": "boolean" },
                  "events": {
                    "type": "array",
                    "minItems": 1,
                    "items": { "$ref": "#/components/schemas/WebhookEvent" }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Subscription updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhook": { "$ref": "#/components/schemas/WebhookSubscription" }
                  }
                }
              }
            }
          },
          "400": { "description": "No updatable fields provided or invalid events" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Webhook subscription not found" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "delete": {
        "summary": "Delete webhook subscription",
        "description": "Remove a webhook subscription. Required scope: `webhooks:write`.",
        "operationId": "deleteWebhook",
        "tags": ["Webhooks"],
        "security": [{ "apiKey": [] }],
        "parameters": [{ "$ref": "#/components/parameters/WebhookId" }],
        "responses": {
          "200": {
            "description": "Subscription deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": { "type": "boolean", "example": true },
                    "id": { "type": "string", "format": "uuid" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Webhook subscription not found" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/keys": {
      "get": {
        "summary": "List API keys",
        "description": "List all API keys for the authenticated workspace. Returns key metadata only — raw key values are never stored or returned after creation. Requires session authentication (not API key) and an Agency plan.",
        "operationId": "listApiKeys",
        "tags": ["API Keys"],
        "security": [{ "session": [] }],
        "responses": {
          "200": {
            "description": "List of API keys",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "keys": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/ApiKeyMetadata" }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "description": "API access requires an Agency plan" }
        }
      },
      "post": {
        "summary": "Create API key",
        "description": "Generate a new API key for the authenticated workspace. The raw key is returned once in the response and cannot be recovered — store it securely. Requires session authentication (not API key) and an Agency plan.",
        "operationId": "createApiKey",
        "tags": ["API Keys"],
        "security": [{ "session": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name", "scopes"],
                "properties": {
                  "name": { "type": "string", "description": "Human-readable key name" },
                  "scopes": {
                    "type": "array",
                    "items": { "$ref": "#/components/schemas/ApiScope" },
                    "description": "Permission scopes for this key"
                  },
                  "agent_name": {
                    "type": "string",
                    "description": "Optional name of the AI agent using this key"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "API key created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string",
                      "description": "Raw API key (shown once only). Format: tap_ak_{32 hex characters}",
                      "example": "tap_ak_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"
                    },
                    "id": { "type": "string", "format": "uuid" }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid request (missing name or scopes)" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "description": "API access requires an Agency plan" }
        }
      },
      "delete": {
        "summary": "Revoke API key",
        "description": "Revoke an active API key. The key will immediately stop working. Requires session authentication (not API key) and an Agency plan.",
        "operationId": "revokeApiKey",
        "tags": ["API Keys"],
        "security": [{ "session": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["keyId"],
                "properties": {
                  "keyId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "ID of the API key to revoke"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "API key revoked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "revoked": { "type": "boolean", "example": true },
                    "keyId": { "type": "string", "format": "uuid" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "description": "API access requires an Agency plan" },
          "404": { "description": "API key not found or already revoked" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Workspace API key. Format: `tap_ak_{32 hex}`. Create keys in TAP Settings > API Keys, or via `POST /api/v1/keys` (Agency plan required). Pass as `Authorization: Bearer tap_ak_...`. Rate limit: 10 requests per minute per key."
      },
      "session": {
        "type": "apiKey",
        "in": "cookie",
        "name": "sb-access-token",
        "description": "Supabase session cookie (browser-based authentication). Used for API key management and release-brief accept/decline endpoints."
      }
    },
    "parameters": {
      "Page": {
        "name": "page",
        "in": "query",
        "schema": { "type": "integer", "minimum": 1, "default": 1 },
        "description": "1-based page number"
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 },
        "description": "Results per page (max 200)"
      },
      "CampaignId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": { "type": "string", "format": "uuid" },
        "description": "Campaign ID"
      },
      "SkillSlug": {
        "name": "slug",
        "in": "path",
        "required": true,
        "schema": { "type": "string" },
        "description": "Skill slug"
      },
      "WebhookId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": { "type": "string", "format": "uuid" },
        "description": "Webhook subscription ID"
      }
    },
    "schemas": {
      "ApiScope": {
        "type": "string",
        "enum": [
          "contacts:read",
          "contacts:write",
          "campaigns:read",
          "campaigns:write",
          "pitches:read",
          "pitches:write",
          "outcomes:read",
          "outcomes:write",
          "webhooks:read",
          "webhooks:write",
          "actions:read",
          "approvals:read",
          "skills:read",
          "skills:write",
          "briefs:write",
          "validate",
          "enrich",
          "*"
        ],
        "description": "Permission scope grantable to an API key. `*` grants all scopes."
      },
      "WebhookEvent": {
        "type": "string",
        "enum": [
          "contact.imported",
          "contact.enriched",
          "campaign.created",
          "campaign.updated",
          "pitch.drafted",
          "pitch.sent",
          "outcome.logged",
          "followup.due",
          "agent.approval_requested",
          "agent.approval_granted",
          "agent.approval_rejected"
        ],
        "description": "Workspace event a webhook subscription can receive"
      },
      "Contact": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "email": { "type": "string", "format": "email" },
          "outlet": {
            "type": "string",
            "description": "Publication, station, or organisation name"
          },
          "role": { "type": "string", "description": "Contact's role or title" },
          "pipeline_status": {
            "type": "string",
            "enum": ["new", "bounced"],
            "description": "Contact pipeline status"
          },
          "enriched": {
            "type": "boolean",
            "description": "Whether AI enrichment has been applied"
          },
          "enriched_at": { "type": "string", "format": "date-time" },
          "platform_type": { "type": "string", "description": "Source platform type" },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "ContactImport": {
        "type": "object",
        "required": ["email"],
        "properties": {
          "email": { "type": "string", "format": "email" },
          "name": {
            "type": "string",
            "description": "Contact name (auto-extracted from email if omitted)"
          },
          "outlet": { "type": "string" },
          "role": { "type": "string" },
          "platform_type": { "type": "string" },
          "genres": {
            "description": "Genre tags. Accepts string[], CSV string, or single string.",
            "oneOf": [{ "type": "array", "items": { "type": "string" } }, { "type": "string" }]
          },
          "geographic_scope": { "type": "string", "description": "Geographic coverage area" },
          "notes": { "type": "string" }
        }
      },
      "SharedContact": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "role": { "type": "string", "nullable": true },
          "outlet": { "type": "string", "nullable": true },
          "email": { "type": "string", "format": "email" },
          "phone": { "type": "string", "nullable": true },
          "country": { "type": "string", "nullable": true },
          "genre_tags": { "type": "array", "items": { "type": "string" } },
          "verification_date": { "type": "string", "format": "date", "nullable": true },
          "source_url": { "type": "string", "nullable": true },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "ValidationResult": {
        "type": "object",
        "properties": {
          "email": { "type": "string" },
          "valid": { "type": "boolean" },
          "confidence": { "type": "string", "enum": ["high", "medium", "low", "none"] },
          "normalised": { "type": "string", "description": "Normalised email address" },
          "reason": { "type": "string", "description": "Reason for invalid status" },
          "corrected": { "type": "boolean", "description": "Whether a correction was applied" },
          "suggestedEmail": {
            "type": "string",
            "description": "Suggested corrected email if applicable"
          },
          "catchAll": { "type": "boolean", "description": "Domain accepts all addresses" },
          "roleBased": {
            "type": "boolean",
            "description": "Generic role-based address (info@, admin@)"
          },
          "disposable": { "type": "boolean", "description": "Disposable email provider" },
          "smtpVerified": { "type": "boolean", "description": "SMTP mailbox verification passed" }
        }
      },
      "EnrichmentResult": {
        "type": "object",
        "properties": {
          "contactId": { "type": "string", "format": "uuid" },
          "status": { "type": "string", "enum": ["enriched", "failed", "skipped"] },
          "fieldsUpdated": { "type": "integer" },
          "error": { "type": "string" },
          "data": {
            "type": "object",
            "description": "Enriched data fields when status is 'enriched'",
            "properties": {
              "platform": { "type": "string" },
              "platform_type": { "type": "string" },
              "role_detail": { "type": "string" },
              "genres": { "type": "array", "items": { "type": "string" } },
              "coverage_area": { "type": "string" },
              "contact_method": { "type": "string" },
              "best_timing": { "type": "string" },
              "submission_guidelines": { "type": "string" },
              "pitch_tips": { "type": "string" },
              "bbc_station": { "type": "string" },
              "geographic_scope": { "type": "string" },
              "enrichment_confidence": { "type": "number", "minimum": 0, "maximum": 100 }
            }
          }
        }
      },
      "CampaignSummary": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "artist_name": { "type": "string" },
          "release_title": { "type": "string" },
          "release_date": { "type": "string", "format": "date" },
          "status": { "type": "string", "enum": ["active", "draft", "paused", "completed"] },
          "services": { "type": "array", "items": { "type": "string" } },
          "contact_count": { "type": "integer" },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "CampaignDetail": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "artist_name": { "type": "string" },
          "release_title": { "type": "string" },
          "release_date": { "type": "string", "format": "date" },
          "status": { "type": "string", "enum": ["active", "draft", "paused", "completed"] },
          "services": { "type": "array", "items": { "type": "string" } },
          "goal": { "type": "string" },
          "embargo_date": { "type": "string", "format": "date" },
          "embargo_notes": { "type": "string" },
          "momentum_score": { "type": "number" },
          "momentum_trend": { "type": "string" },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "CampaignStats": {
        "type": "object",
        "properties": {
          "contact_count": { "type": "integer", "description": "Total contacts in campaign" },
          "pitched_count": { "type": "integer", "description": "Contacts that have been pitched" },
          "replied_count": { "type": "integer", "description": "Contacts that have replied" },
          "coverage_count": {
            "type": "integer",
            "description": "Contacts marked as supporting/done"
          }
        }
      },
      "ActivityEntry": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "signal_type": { "type": "string" },
          "source_app": { "type": "string" },
          "payload": { "type": "object" },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "CampaignContactEntry": {
        "type": "object",
        "properties": {
          "campaign_contact_id": { "type": "string", "format": "uuid" },
          "pitch_status": {
            "type": "string",
            "enum": ["not_pitched", "draft", "sent", "opened", "replied", "declined", "bounced"]
          },
          "engagement_state": { "type": "string", "enum": ["idle", "active", "paused", "done"] },
          "notes": { "type": "string" },
          "added_at": { "type": "string", "format": "date-time" },
          "last_pitched_at": { "type": "string", "format": "date-time" },
          "last_contacted_at": { "type": "string", "format": "date-time" },
          "follow_up_due": { "type": "string", "format": "date-time" },
          "contact": { "$ref": "#/components/schemas/Contact" }
        }
      },
      "WorkspaceOutcome": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "campaign_id": { "type": "string", "format": "uuid" },
          "contact_id": { "type": "string", "format": "uuid" },
          "outcome_type": {
            "type": "string",
            "enum": [
              "no_response",
              "opened",
              "replied_positive",
              "replied_negative",
              "added_to_playlist",
              "interview_booked",
              "coverage",
              "declined",
              "other"
            ]
          },
          "channel": { "type": "string", "nullable": true },
          "notes": { "type": "string", "nullable": true },
          "occurred_at": { "type": "string", "format": "date-time" },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "OutreachQueueItem": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "source": { "type": "string", "enum": ["pitch_draft", "sequence_execution"] },
          "campaign_id": { "type": "string", "format": "uuid", "nullable": true },
          "campaign_contact_id": { "type": "string", "format": "uuid", "nullable": true },
          "channel": {
            "type": "string",
            "enum": ["email", "portal", "instagram", "linkedin", "physical_post"]
          },
          "state": { "type": "string", "description": "Approval or sequence state" },
          "send_status": { "type": "string", "nullable": true },
          "dispatch_attempts": { "type": "integer" },
          "scheduled_for": { "type": "string", "format": "date-time", "nullable": true },
          "executed_at": { "type": "string", "format": "date-time", "nullable": true },
          "gmail_message_id": { "type": "string", "nullable": true },
          "gmail_thread_id": { "type": "string", "nullable": true },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "ApprovalQueueItem": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "campaign_id": { "type": "string", "format": "uuid" },
          "campaign_name": { "type": "string", "nullable": true },
          "artist_name": { "type": "string", "nullable": true },
          "title": { "type": "string", "nullable": true },
          "subject": { "type": "string", "nullable": true },
          "body_preview": {
            "type": "string",
            "description": "First 240 characters of the draft body"
          },
          "body_length": { "type": "integer" },
          "send_status": { "type": "string" },
          "approval_state": { "type": "string", "example": "pending" },
          "source": { "type": "string" },
          "channel": { "type": "string", "example": "email" },
          "scheduled_at": { "type": "string", "format": "date-time", "nullable": true },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "Artist": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "genre": { "type": "string", "nullable": true },
          "bio": { "type": "string", "nullable": true },
          "spotify_url": { "type": "string", "nullable": true },
          "apple_music_url": { "type": "string", "nullable": true },
          "website": { "type": "string", "nullable": true },
          "instagram": { "type": "string", "nullable": true },
          "twitter": { "type": "string", "nullable": true },
          "archived_at": { "type": "string", "format": "date-time", "nullable": true },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "SkillSummary": {
        "type": "object",
        "properties": {
          "slug": { "type": "string" },
          "name": { "type": "string" },
          "description": { "type": "string" },
          "category": { "type": "string" },
          "version": { "type": "string" },
          "enabled": { "type": "boolean" },
          "source": {
            "type": "string",
            "enum": ["workspace", "default"],
            "description": "Whether the workspace has an override body"
          },
          "body_length": { "type": "integer" }
        }
      },
      "SkillDetail": {
        "type": "object",
        "properties": {
          "slug": { "type": "string" },
          "name": { "type": "string" },
          "description": { "type": "string" },
          "category": { "type": "string" },
          "tags": { "type": "array", "items": { "type": "string" } },
          "version": { "type": "string" },
          "type": { "type": "string", "description": "Skill type from the manifest" },
          "tools": { "type": "array", "items": { "type": "string" } },
          "permissions": { "type": "object" },
          "io": {
            "type": "object",
            "description": "Declared input/output shape for the skill"
          },
          "body": { "type": "string", "description": "Effective skill body markdown" },
          "source": { "type": "string", "enum": ["workspace", "default"] }
        }
      },
      "SkillRunResult": {
        "type": "object",
        "properties": {
          "skill_slug": { "type": "string" },
          "invocation_id": { "type": "string", "format": "uuid", "nullable": true },
          "ok": { "type": "boolean" },
          "output": { "type": "object", "nullable": true },
          "error": { "type": "string", "nullable": true },
          "duration_ms": { "type": "integer" },
          "tokens_used": { "type": "integer", "nullable": true },
          "confidence": { "type": "number", "nullable": true }
        }
      },
      "ReleaseBriefPayload": {
        "type": "object",
        "required": [
          "source_brief_id",
          "source_label_id",
          "source_label_name",
          "artist",
          "release",
          "tracks",
          "partner",
          "body_markdown",
          "asset_pack"
        ],
        "properties": {
          "source_brief_id": {
            "type": "string",
            "format": "uuid",
            "description": "Brief ID in the sending system — used for idempotency"
          },
          "source_label_id": { "type": "string", "format": "uuid" },
          "source_label_name": { "type": "string", "minLength": 1 },
          "artist": {
            "type": "object",
            "required": ["name"],
            "properties": {
              "name": { "type": "string", "minLength": 1 },
              "primary_image_url": { "type": "string", "format": "uri" }
            }
          },
          "release": {
            "type": "object",
            "required": ["title", "type", "target_release_at"],
            "properties": {
              "title": { "type": "string", "minLength": 1 },
              "type": { "type": "string", "enum": ["single", "ep", "album"] },
              "target_release_at": {
                "type": "string",
                "description": "ISO date or date-time with offset"
              },
              "target_announce_at": {
                "type": "string",
                "description": "ISO date or date-time with offset"
              },
              "genre_tags": {
                "type": "array",
                "items": { "type": "string" },
                "default": []
              }
            }
          },
          "tracks": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "object",
              "required": ["position", "title"],
              "properties": {
                "position": { "type": "integer", "minimum": 1 },
                "title": { "type": "string", "minLength": 1 },
                "isrc": { "type": "string" },
                "duration_seconds": { "type": "integer", "minimum": 1 }
              }
            }
          },
          "partner": {
            "type": "object",
            "required": ["name", "kind"],
            "properties": {
              "name": { "type": "string", "minLength": 1 },
              "kind": {
                "type": "string",
                "enum": ["pr", "radio", "sync", "digital", "other"]
              }
            }
          },
          "body_markdown": {
            "type": "string",
            "minLength": 1,
            "description": "The brief itself, in markdown"
          },
          "asset_pack": {
            "type": "object",
            "required": ["drive_folder_url"],
            "properties": {
              "drive_folder_url": { "type": "string", "format": "uri" },
              "file_list": {
                "type": "array",
                "default": [],
                "items": {
                  "type": "object",
                  "required": ["kind", "label"],
                  "properties": {
                    "kind": {
                      "type": "string",
                      "enum": [
                        "master",
                        "artwork",
                        "bio",
                        "photo",
                        "press_note",
                        "epk_pdf",
                        "other"
                      ]
                    },
                    "label": { "type": "string", "minLength": 1 },
                    "url": { "type": "string", "format": "uri" }
                  }
                }
              }
            }
          },
          "embargo": { "type": "boolean", "default": false },
          "notes": { "type": "string" },
          "callback_url": {
            "type": "string",
            "format": "uri",
            "description": "Optional URL notified when the brief is accepted or declined"
          }
        }
      },
      "ReleaseBriefReceipt": {
        "type": "object",
        "properties": {
          "inbox_release_brief_id": { "type": "string", "format": "uuid" },
          "accept_url": {
            "type": "string",
            "format": "uri",
            "description": "TAP inbox URL where a human reviews the brief"
          },
          "status": {
            "type": "string",
            "enum": ["pending", "accepted", "declined"],
            "description": "Current brief status"
          }
        }
      },
      "WebhookSubscription": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "url": { "type": "string", "format": "uri" },
          "events": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/WebhookEvent" }
          },
          "active": { "type": "boolean" },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "ApiKeyMetadata": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "agent_name": { "type": "string" },
          "scopes": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/ApiScope" }
          },
          "key_prefix": {
            "type": "string",
            "description": "First characters of the key for identification",
            "example": "tap_ak_a1b2"
          },
          "last_used_at": { "type": "string", "format": "date-time" },
          "created_at": { "type": "string", "format": "date-time" },
          "revoked_at": { "type": "string", "format": "date-time" }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Invalid or missing authentication. API key endpoints require `Authorization: Bearer tap_ak_...` header.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": { "type": "string", "example": "Invalid or missing API key" }
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "API key does not have the required scope for this operation.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "string",
                  "example": "Insufficient scope: contacts:read required"
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded — 10 requests per minute per API key. Check the X-RateLimit-* response headers for the reset time.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": { "type": "string", "example": "Rate limit exceeded" }
              }
            }
          }
        }
      }
    }
  }
}
