{
  "openapi": "3.0.3",
  "info": {
    "title": "Affset API",
    "version": "1.0.0",
    "description": "REST API of Affset, the white-label ad server and CPA network platform. Manage campaigns, zones, targeting, payouts, team members and tenant settings, and read stats and conversions. The public ad-serving endpoints (`/serve`, `/track/click`, `/px`) are the tracking surface you hand to traffic sources and advertisers.\n\n## Authentication\n\nEvery authenticated endpoint takes two headers:\n\n- `Authorization: Bearer <token>` — a tenant API key, or the 30-day session token issued by magic-link login. Both authenticate identically.\n- `X-Namespace: <namespace>` — the tenant the token belongs to.\n\nKeys carry `permissions`: `read` covers GET, `write` is required for every POST, PUT, PATCH and DELETE (each operation's `x-affset-permission` says which). Within the tenant, what a key can see and change is decided by its role:\n\n| Role | Access |\n| --- | --- |\n| `owner` | Full access to everything. The only role that can permanently delete the tenant. |\n| `manager` | Same day-to-day access as owner — campaigns, zones, team, payouts, targeting. Can’t delete the tenant. |\n| `advertiser` | Manages their own campaigns and can read zones. Sees campaign spend, but not publisher payout, media cost, or ROI. |\n| `advertiser_manager` | Manages campaigns for assigned advertisers and can add advertisers to their own team. Uses the same financial redaction as advertiser. |\n| `publisher` | Manages their own zones and has no campaign access. Sees payout, media cost, and ROI, but not advertiser spend. |\n| `publisher_manager` | Manages zones for assigned publishers and can add publishers to their own team. Uses the same financial redaction as publisher. |\n\n`GET /api/me` reports the role, permissions and a capabilities summary for the token in use.\n\n- `read` — GET endpoints — list and read resources, stats, and conversions.\n- `write` — POST, PUT, PATCH and DELETE — create, update, delete, rotate, revoke.\n\nThere is no OAuth flow on this REST API itself — tokens are API keys. The hosted MCP server at https://mcp.affset.com/mcp is the OAuth-protected surface over the same API; its scopes map onto the key permissions above and are declared machine-readably in its RFC 9728 metadata (https://mcp.affset.com/.well-known/oauth-protected-resource) and RFC 8414 metadata (https://oauth.affset.com/.well-known/oauth-authorization-server):\n\n- scope `read` → key permissions `read`. Read-only tool set — every tool that writes is stripped from the session.\n- scope `full` → key permissions `read`, `write`. Every tool, backed by a read+write key. Only offered to roles that can write.\n\n## Errors\n\nErrors are JSON: `{ \"error\": \"human-readable message\" }`, sometimes with extra machine-readable fields (see the `PlanLimitError` schema for 402).\n\n- **400** Bad Request — missing header, parameter, or body, or an invalid value.\n- **401** Unauthorized — invalid or expired key, or X-Namespace doesn't match it.\n- **402** Payment Required — you’re at a plan limit. See below.\n- **403** Forbidden — your role or permissions don't allow this. Also used to mask ownership on writes.\n- **404** Not Found — the resource doesn't exist, or isn't visible to your role.\n- **409** Conflict — something with the same identity already exists.\n- **500** Internal Server Error — something went wrong on our end.\n\n## Other formats\n\n- Human reference: https://affset.com/docs (operation `x-affset-docs` links point at the matching section)\n- Markdown feed for assistants: https://affset.com/api-reference.md\n- Structured feed: https://affset.com/api-reference.json\n- Site index for crawlers and agents: https://affset.com/llms.txt\n- MCP server (typed tools over this API): https://mcp.affset.com/mcp — source https://github.com/affset/mcp",
    "termsOfService": "https://affset.com/terms",
    "contact": {
      "name": "Affset",
      "url": "https://affset.com/contact"
    }
  },
  "externalDocs": {
    "description": "Affset API reference",
    "url": "https://affset.com/docs"
  },
  "servers": [
    {
      "url": "https://api.affset.com",
      "description": "Production. Tenants with a custom API domain (PUT /api/tenant custom_api_domain) also serve the public ad-serving endpoints from that domain."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Auth",
      "description": "Verify a token and see what it can do."
    },
    {
      "name": "Tenant settings",
      "description": "Branding, timezone, sub-label names, and a couple of serving behaviors for your account."
    },
    {
      "name": "Stats",
      "description": "Aggregated performance, grouped by one dimension per call."
    },
    {
      "name": "Campaigns",
      "description": "Owner and manager see every campaign. advertiser and advertiser_manager are scoped to their own or managed advertisers. publisher and publisher_manager get 403 on this entire tree."
    },
    {
      "name": "Zones",
      "description": "Owner, manager, publisher and publisher_manager can create/update zones — publisher and publisher_manager scoped to their own or managed publishers. advertiser and advertiser_manager have read-only access."
    },
    {
      "name": "Team",
      "description": "Team members and machine API keys share one endpoint, distinguished by type. There’s no separate /api/team."
    },
    {
      "name": "Payout rules",
      "description": "Same campaign-scoping as Campaigns above. A campaign can have one global rule and one rule per zone."
    },
    {
      "name": "Targeting",
      "description": "Same campaign-scoping as Campaigns above. Enforced on /serve only — never on a direct /track/click link."
    },
    {
      "name": "Conversions",
      "description": "The conversion audit trail — individual records, not aggregates."
    },
    {
      "name": "Ad serving",
      "description": "Public — no Authorization or X-Namespace header. Namespace is resolved from the zone/campaign in the URL. On error these return plain text, not JSON."
    }
  ],
  "paths": {
    "/api/me": {
      "get": {
        "operationId": "me",
        "summary": "Verify a key and see what it can do",
        "description": "- The response also includes capabilities, a role-derived summary of what this key can do — handy for building conditional UI without hard-coding the role table above.\n\nDocs: https://affset.com/docs#me",
        "tags": [
          "Auth"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/XNamespace"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "namespace": {
                      "type": "string"
                    },
                    "user_id": {
                      "type": "string"
                    },
                    "email": {
                      "type": "string",
                      "format": "email"
                    },
                    "role": {
                      "type": "string"
                    },
                    "permissions": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "capabilities": {
                      "type": "object",
                      "properties": {
                        "campaigns": {
                          "type": "boolean"
                        },
                        "zones": {
                          "type": "boolean"
                        },
                        "assign_campaign_user": {
                          "type": "boolean"
                        },
                        "assign_zone_user": {
                          "type": "boolean"
                        },
                        "tenant_management": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "namespace": "acme-media",
                  "user_id": "usr_8f2a1c",
                  "email": "buyer@example.com",
                  "role": "advertiser",
                  "permissions": [
                    "read",
                    "write"
                  ],
                  "capabilities": {
                    "campaigns": true,
                    "zones": true,
                    "assign_campaign_user": false,
                    "assign_zone_user": false,
                    "tenant_management": false
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#me"
        ],
        "x-affset-permission": "read"
      }
    },
    "/api/tenant": {
      "get": {
        "operationId": "tenantGet",
        "summary": "Read tenant settings",
        "description": "- There’s also an unauthenticated GET /api/public/tenant (X-Namespace header only, no API key) that returns just company, primary_color and secondary_color — enough to brand a public-facing page.\n\nDocs: https://affset.com/docs#tenant-get",
        "tags": [
          "Tenant settings"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/XNamespace"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "company": {
                      "type": "string"
                    },
                    "timezone": {
                      "type": "string"
                    },
                    "primary_color": {
                      "type": "string"
                    },
                    "secondary_color": {
                      "type": "string"
                    },
                    "custom_api_domain": {
                      "type": "string"
                    },
                    "sub_labels": {
                      "type": "object",
                      "properties": {
                        "sub1": {
                          "type": "string"
                        },
                        "sub2": {
                          "type": "string"
                        }
                      }
                    },
                    "redirect_method": {
                      "type": "string"
                    },
                    "email": {
                      "type": "string",
                      "format": "email"
                    },
                    "feature_flags": {
                      "type": "object",
                      "properties": {}
                    }
                  }
                },
                "example": {
                  "company": "Acme Media",
                  "timezone": "America/New_York",
                  "primary_color": "#4F46E5",
                  "secondary_color": "#14161C",
                  "custom_api_domain": "api.acme-media.com",
                  "sub_labels": {
                    "sub1": "Zone",
                    "sub2": "Creative"
                  },
                  "redirect_method": "html",
                  "email": "owner@acme-media.com",
                  "feature_flags": {}
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#tenant-get"
        ],
        "x-affset-permission": "read"
      },
      "put": {
        "operationId": "tenantUpdate",
        "summary": "Update tenant settings",
        "description": "- At least one field is required.\n- sub_labels is a merge, not a replace — send only the keys you want to change. null or \"\" clears that slot. Max 40 characters per label; an unrecognized key like sub6 is a 400, not a silent no-op.\n- Setting a non-empty custom_api_domain is plan-gated and can return 402; clearing it is always free.\n\nDocs: https://affset.com/docs#tenant-update",
        "tags": [
          "Tenant settings"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/XNamespace"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "company": {
                      "type": "string"
                    },
                    "timezone": {
                      "type": "string"
                    },
                    "redirect_method": {
                      "type": "string"
                    },
                    "sub_labels": {
                      "type": "object",
                      "properties": {
                        "sub1": {
                          "type": "string"
                        }
                      }
                    },
                    "updated_at": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "company": "Acme Media",
                  "timezone": "America/New_York",
                  "redirect_method": "3xx",
                  "sub_labels": {
                    "sub1": "Zone"
                  },
                  "updated_at": 1753887600000
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "402": {
            "$ref": "#/components/responses/Error402"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#tenant-update"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "company": {
                    "type": "string",
                    "description": "Display name shown in the dashboard and emails."
                  },
                  "timezone": {
                    "type": "string",
                    "description": "IANA timezone, e.g. \"America/New_York\". Drives Stats date bucketing and campaign date-only schedules."
                  },
                  "primary_color": {
                    "type": "string",
                    "description": "Hex color, e.g. \"#4F46E5\". Must match #RRGGBB."
                  },
                  "secondary_color": {
                    "type": "string",
                    "description": "Hex color, e.g. \"#14161C\". Must match #RRGGBB."
                  },
                  "custom_api_domain": {
                    "type": "string",
                    "description": "Domain used in generated /serve and /track/click links instead of the default API host. Setting a non-empty value is plan-gated; clearing it is always free."
                  },
                  "sub_labels": {
                    "type": "object",
                    "properties": {
                      "sub1": {
                        "type": "string",
                        "nullable": true,
                        "maxLength": 40,
                        "description": "Display name for sub1; null or \"\" clears it."
                      },
                      "sub2": {
                        "type": "string",
                        "nullable": true,
                        "maxLength": 40,
                        "description": "Display name for sub2; null or \"\" clears it."
                      },
                      "sub3": {
                        "type": "string",
                        "nullable": true,
                        "maxLength": 40,
                        "description": "Display name for sub3; null or \"\" clears it."
                      },
                      "sub4": {
                        "type": "string",
                        "nullable": true,
                        "maxLength": 40,
                        "description": "Display name for sub4; null or \"\" clears it."
                      },
                      "sub5": {
                        "type": "string",
                        "nullable": true,
                        "maxLength": 40,
                        "description": "Display name for sub5; null or \"\" clears it."
                      }
                    },
                    "additionalProperties": false,
                    "description": "Partial update of sub1–sub5 display names — see below. Only the keys you send are touched."
                  },
                  "redirect_method": {
                    "type": "string",
                    "enum": [
                      "html",
                      "3xx"
                    ],
                    "description": "How the /serve → /track/click hop is delivered — see Ad serving."
                  }
                },
                "minProperties": 1
              },
              "example": {
                "sub_labels": {
                  "sub1": "Zone",
                  "sub3": null
                },
                "redirect_method": "3xx"
              }
            }
          }
        },
        "x-affset-permission": "write"
      }
    },
    "/api/stats": {
      "get": {
        "operationId": "statsGet",
        "summary": "Grouped traffic and conversion stats",
        "description": "- group_by=publisher_email needs owner, manager, or publisher_manager (scoped to their assigned publishers); group_by=advertiser_email needs owner, manager, or advertiser_manager (scoped to their assigned advertisers). The row key is the current zone/campaign owner — reassigning a zone or campaign re-attributes its history.\n- advertiser_email and publisher_email are also standalone filters (not just group_by values): passed as their own query params they narrow every row to one advertiser/publisher regardless of what group_by is set to, instead of breaking every user out into its own row. Same RBAC as the matching group_by.\n- from and to are UTC epoch milliseconds. With group_by=date, each returned date label is a calendar date in your tenant’s timezone (set via PUT /api/tenant).\n- Two different costs show up here: spend is Affset’s own campaign accounting — CPM campaigns accrue it per click at the campaign’s rate, CPA campaigns accrue it on conversion instead. media_cost is whatever you imported via the cost= parameter on /serve or /track/click — your actual traffic cost. roi is computed from payout and media_cost, not spend, since that’s the number a media buyer is optimizing against — and it’s null (not 0) when there’s no cost data for that row.\n- Publisher-side roles (publisher, publisher_manager) never see spend. Advertiser-side roles (advertiser, advertiser_manager) never see payout, media_cost, or roi. Same redaction applies to Conversions.\n\nDocs: https://affset.com/docs#stats-get",
        "tags": [
          "Stats"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/XNamespace"
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Default: start of today, UTC. Unix time in milliseconds.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Default: now. Unix time in milliseconds.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "group_by",
            "in": "query",
            "required": false,
            "description": "date (default) | campaign_id | zone_id | country | conversion_type | publisher_email | advertiser_email | sub1…sub5.",
            "schema": {
              "type": "string",
              "enum": [
                "date",
                "campaign_id",
                "zone_id",
                "country",
                "conversion_type",
                "publisher_email",
                "advertiser_email",
                "sub1",
                "sub2",
                "sub3",
                "sub4",
                "sub5"
              ],
              "default": "date"
            },
            "example": "zone_id"
          },
          {
            "name": "campaign_ids",
            "in": "query",
            "required": false,
            "description": "Restrict to these campaigns. Comma-separated list.",
            "schema": {
              "type": "string"
            },
            "example": "42"
          },
          {
            "name": "zone_ids",
            "in": "query",
            "required": false,
            "description": "Restrict to these zones. Comma-separated list.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "publisher_manager_email",
            "in": "query",
            "required": false,
            "description": "Restrict to zones owned by publishers assigned to this manager. Owner/manager may use any manager email; publisher_manager may use only their own.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "advertiser_email",
            "in": "query",
            "required": false,
            "description": "Narrow every row to one advertiser’s campaigns, independent of group_by. Owner/manager may use any advertiser email; advertiser_manager only one of their own assigned advertisers.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "publisher_email",
            "in": "query",
            "required": false,
            "description": "Narrow every row to one publisher’s zones, independent of group_by. Owner/manager may use any publisher email; publisher_manager only one of their own assigned publishers.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub1",
            "in": "query",
            "required": false,
            "description": "A value, a comma list, or an empty string to match rows where that sub is unset.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub2",
            "in": "query",
            "required": false,
            "description": "A value, a comma list, or an empty string to match rows where that sub is unset.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub3",
            "in": "query",
            "required": false,
            "description": "A value, a comma list, or an empty string to match rows where that sub is unset.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub4",
            "in": "query",
            "required": false,
            "description": "A value, a comma list, or an empty string to match rows where that sub is unset.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub5",
            "in": "query",
            "required": false,
            "description": "A value, a comma list, or an empty string to match rows where that sub is unset.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "stats": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "zone_id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "zone_name": {
                            "type": "string"
                          },
                          "impressions": {
                            "type": "integer"
                          },
                          "fallbacks": {
                            "type": "integer"
                          },
                          "unsold": {
                            "type": "integer"
                          },
                          "clicks": {
                            "type": "integer"
                          },
                          "conversions": {
                            "type": "integer"
                          },
                          "spend": {
                            "type": "number"
                          },
                          "payout": {
                            "type": "number"
                          },
                          "media_cost": {
                            "type": "number"
                          },
                          "roi": {
                            "type": "number"
                          }
                        }
                      }
                    },
                    "period": {
                      "type": "object",
                      "properties": {
                        "from": {
                          "type": "integer"
                        },
                        "to": {
                          "type": "integer"
                        }
                      }
                    },
                    "sub_labels": {
                      "type": "object",
                      "properties": {
                        "sub1": {
                          "type": "string"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "stats": [
                    {
                      "zone_id": "b6e1e6b0-2f2a-4a3e-9c8e-1c9a2f6d4b11",
                      "zone_name": "RichAds Push",
                      "impressions": 18400,
                      "fallbacks": 210,
                      "unsold": 40,
                      "clicks": 512,
                      "conversions": 9,
                      "spend": 12.8,
                      "payout": 27.5,
                      "media_cost": 14.2,
                      "roi": 0.9366
                    }
                  ],
                  "period": {
                    "from": 1753747200000,
                    "to": 1753833600000
                  },
                  "sub_labels": {
                    "sub1": "Zone"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#stats-get"
        ],
        "x-affset-permission": "read"
      }
    },
    "/api/campaigns": {
      "get": {
        "operationId": "campaignsList",
        "summary": "List campaigns",
        "description": "- There’s no server-side name search — filter the page client-side if you need it.\n\nDocs: https://affset.com/docs#campaigns-list",
        "tags": [
          "Campaigns"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/XNamespace"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "active | paused | archived. Omit for all statuses.",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "paused",
                "archived"
              ]
            },
            "example": "active"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "1–100, default 20.",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Default 0.",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "name | created_at (default) | start_date.",
            "schema": {
              "type": "string",
              "enum": [
                "name",
                "created_at",
                "start_date"
              ],
              "default": "created_at"
            }
          },
          {
            "name": "order",
            "in": "query",
            "required": false,
            "description": "asc | desc (default).",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "campaigns": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          },
                          "redirect_url": {
                            "type": "string"
                          },
                          "redirect_urls": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "payment_model": {
                            "type": "string"
                          },
                          "rate": {
                            "type": "integer"
                          },
                          "payout_goal_type": {},
                          "daily_budget": {},
                          "total_budget": {},
                          "pacing": {
                            "type": "string"
                          },
                          "start_date": {
                            "type": "integer"
                          },
                          "end_date": {},
                          "user_email": {
                            "type": "string",
                            "format": "email"
                          },
                          "created_at": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        },
                        "has_more": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "campaigns": [
                    {
                      "id": 42,
                      "name": "BR Sweepstakes — Push",
                      "status": "active",
                      "redirect_url": "https://offer.example/lp?s={click_id}",
                      "redirect_urls": [
                        "https://offer.example/lp?s={click_id}"
                      ],
                      "payment_model": "cpa",
                      "rate": 0,
                      "payout_goal_type": null,
                      "daily_budget": null,
                      "total_budget": null,
                      "pacing": "asap",
                      "start_date": 1753747200000,
                      "end_date": null,
                      "user_email": "buyer@example.com",
                      "created_at": 1753747200000
                    }
                  ],
                  "pagination": {
                    "total": 7,
                    "limit": 20,
                    "offset": 0,
                    "has_more": false
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#campaigns-list"
        ],
        "x-affset-permission": "read"
      },
      "post": {
        "operationId": "campaignsCreate",
        "summary": "Create a campaign",
        "description": "- New campaigns are always created paused — run it with PUT once it’s ready (see Update below).\n\nDocs: https://affset.com/docs#campaigns-create",
        "tags": [
          "Campaigns"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/XNamespace"
          }
        ],
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string"
                    },
                    "redirect_url": {
                      "type": "string"
                    },
                    "redirect_urls": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "status": {
                      "type": "string"
                    },
                    "payout_goal_type": {},
                    "silent": {
                      "type": "integer"
                    },
                    "created_at": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "id": 42,
                  "name": "BR Sweepstakes — Push",
                  "redirect_url": "https://offer.example/lp?s={click_id}",
                  "redirect_urls": [
                    "https://offer.example/lp?s={click_id}"
                  ],
                  "status": "paused",
                  "payout_goal_type": null,
                  "silent": 0,
                  "created_at": 1753747200000
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#campaigns-create"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Campaign name."
                  },
                  "redirect_url": {
                    "type": "string",
                    "description": "Must be http(s). See Ad serving for the macros it can contain. Either this or redirect_urls is required."
                  },
                  "redirect_urls": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "1–10 http(s) URLs; clicks are split randomly between them (prelander rotation). Wins over redirect_url when both are sent. Repeat a URL to give it a bigger share."
                  },
                  "user_email": {
                    "type": "string",
                    "description": "Required for owner, manager and advertiser_manager — whose advertiser this bills to. Advertisers may omit it (defaults to themselves)."
                  },
                  "payment_model": {
                    "type": "string",
                    "enum": [
                      "cpm",
                      "cpa"
                    ],
                    "default": "cpm",
                    "description": "Default cpm."
                  },
                  "rate": {
                    "type": "number",
                    "default": 0,
                    "minimum": 0,
                    "description": "Default 0. Rounded to 2 decimals."
                  },
                  "payout_goal_type": {
                    "type": "string",
                    "nullable": true,
                    "description": "Only conversions whose pixel type= matches this exactly accrue spend/payout — others record at $0."
                  },
                  "silent": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "0 disables silent conversions. A positive N makes every Nth conversion pay $0 and skip the affiliate postback; requires the silent_conversions feature flag."
                  },
                  "daily_budget": {
                    "type": "number",
                    "nullable": true,
                    "description": "0–999999999.99999."
                  },
                  "total_budget": {
                    "type": "number",
                    "nullable": true,
                    "description": "0–999999999.99999."
                  },
                  "pacing": {
                    "type": "string",
                    "enum": [
                      "asap",
                      "even"
                    ],
                    "default": "asap",
                    "description": "Default asap."
                  },
                  "start_date": {
                    "type": "integer",
                    "description": "Optional. Unix time in milliseconds."
                  },
                  "end_date": {
                    "type": "integer",
                    "description": "Optional. Unix time in milliseconds."
                  },
                  "targeting_rules": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/TargetingRuleInput"
                    },
                    "description": "Optional shortcut: [{ targeting_rule_type_id, targeting_method, rule }] — same shape as the Targeting sync endpoint. Convenient for geo at creation time; everything else, use Targeting after creating."
                  }
                },
                "required": [
                  "name",
                  "redirect_url"
                ]
              },
              "example": {
                "name": "BR Sweepstakes — Push",
                "redirect_url": "https://offer.example/lp?s={click_id}",
                "user_email": "buyer@example.com",
                "payment_model": "cpa",
                "targeting_rules": [
                  {
                    "targeting_rule_type_id": 1,
                    "targeting_method": "whitelist",
                    "rule": "BR,MX"
                  }
                ]
              }
            }
          }
        },
        "x-affset-permission": "write"
      }
    },
    "/api/campaigns/{campaign_id}": {
      "get": {
        "operationId": "campaignsGet",
        "summary": "Get one campaign",
        "description": "- 404 for a campaign that doesn’t exist or isn’t visible to your role — GET never reveals which.\n\nDocs: https://affset.com/docs#campaigns-get",
        "tags": [
          "Campaigns"
        ],
        "parameters": [
          {
            "name": "campaign_id",
            "in": "path",
            "required": true,
            "description": "Campaign id.",
            "schema": {
              "type": "integer"
            },
            "example": 42
          },
          {
            "$ref": "#/components/parameters/XNamespace"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    },
                    "redirect_url": {
                      "type": "string"
                    },
                    "redirect_urls": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "payment_model": {
                      "type": "string"
                    },
                    "rate": {
                      "type": "integer"
                    },
                    "user_email": {
                      "type": "string",
                      "format": "email"
                    },
                    "targeting_rules": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "targeting_rule_type_id": {
                            "type": "integer"
                          },
                          "targeting_method": {
                            "type": "string"
                          },
                          "rule": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "id": 42,
                  "name": "BR Sweepstakes — Push",
                  "status": "active",
                  "redirect_url": "https://offer.example/lp?s={click_id}",
                  "redirect_urls": [
                    "https://offer.example/lp?s={click_id}",
                    "https://offer.example/lp-b?s={click_id}"
                  ],
                  "payment_model": "cpa",
                  "rate": 0,
                  "user_email": "buyer@example.com",
                  "targeting_rules": [
                    {
                      "id": 501,
                      "targeting_rule_type_id": 1,
                      "targeting_method": "whitelist",
                      "rule": "BR,MX"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#campaigns-get"
        ],
        "x-affset-permission": "read"
      },
      "put": {
        "operationId": "campaignsUpdate",
        "summary": "Update a campaign (partial)",
        "description": "- Only the fields you send are changed — this is a partial update despite the PUT verb. An empty/unrecognized body is a 400.\n- Setting status to active runs your plan’s active-campaign check and can return 402.\n- Response is just { id, updated_at } — re-fetch with GET if you need the full row back.\n\nDocs: https://affset.com/docs#campaigns-update",
        "tags": [
          "Campaigns"
        ],
        "parameters": [
          {
            "name": "campaign_id",
            "in": "path",
            "required": true,
            "description": "Campaign id.",
            "schema": {
              "type": "integer"
            },
            "example": 42
          },
          {
            "$ref": "#/components/parameters/XNamespace"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "updated_at": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "id": 42,
                  "updated_at": 1753887600000
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "402": {
            "$ref": "#/components/responses/Error402"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#campaigns-update"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "New campaign name."
                  },
                  "redirect_url": {
                    "type": "string",
                    "description": "New http(s) destination; supports the macros listed under Ad serving. Replaces the whole rotation set with this one URL."
                  },
                  "redirect_urls": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "1–10 http(s) URLs; clicks are split randomly between them. Replaces the existing set; wins over redirect_url when both are sent."
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "active",
                      "paused",
                      "archived"
                    ],
                    "description": "Use this to run/pause/archive a campaign."
                  },
                  "payment_model": {
                    "type": "string",
                    "enum": [
                      "cpm",
                      "cpa"
                    ],
                    "description": "How campaign spend is calculated."
                  },
                  "rate": {
                    "type": "number",
                    "minimum": 0,
                    "description": "Non-negative; rounded to 2 decimals."
                  },
                  "payout_goal_type": {
                    "type": "string",
                    "nullable": true,
                    "description": "Send null or an empty string to clear the goal filter."
                  },
                  "silent": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Non-negative silent-conversion cadence; 0 disables it. A positive value requires the silent_conversions feature flag."
                  },
                  "daily_budget": {
                    "type": "number",
                    "nullable": true,
                    "description": "Send null to clear a budget."
                  },
                  "total_budget": {
                    "type": "number",
                    "nullable": true,
                    "description": "Send null to clear a budget."
                  },
                  "pacing": {
                    "type": "string",
                    "enum": [
                      "asap",
                      "even"
                    ],
                    "description": "Delivery pacing; even works against the daily budget."
                  },
                  "start_date": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Send null to clear a boundary. Unix time in milliseconds."
                  },
                  "end_date": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Send null to clear a boundary. Unix time in milliseconds."
                  },
                  "user_email": {
                    "type": "string",
                    "description": "advertiser_email works identically — both write the same field."
                  }
                },
                "minProperties": 1
              },
              "example": {
                "status": "active"
              }
            }
          }
        },
        "x-affset-permission": "write"
      },
      "delete": {
        "operationId": "campaignsDelete",
        "summary": "Delete a campaign",
        "description": "- Cascades its targeting rules. Conversions and click history are not deleted with it.\n\nDocs: https://affset.com/docs#campaigns-delete",
        "tags": [
          "Campaigns"
        ],
        "parameters": [
          {
            "name": "campaign_id",
            "in": "path",
            "required": true,
            "description": "Campaign id.",
            "schema": {
              "type": "integer"
            },
            "example": 42
          },
          {
            "$ref": "#/components/parameters/XNamespace"
          }
        ],
        "responses": {
          "204": {
            "description": "No Content"
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#campaigns-delete"
        ],
        "x-affset-permission": "write"
      }
    },
    "/api/zones": {
      "get": {
        "operationId": "zonesList",
        "summary": "List zones",
        "description": "- manager_email is computed, not stored — it’s the publisher_manager who owns that zone’s publisher, if any.\n\nDocs: https://affset.com/docs#zones-list",
        "tags": [
          "Zones"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/XNamespace"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "active | inactive.",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "inactive"
              ]
            },
            "example": "active"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "1–100, default 20.",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Default 0.",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "name | created_at (default) | site_url.",
            "schema": {
              "type": "string",
              "enum": [
                "name",
                "created_at",
                "site_url"
              ],
              "default": "created_at"
            }
          },
          {
            "name": "order",
            "in": "query",
            "required": false,
            "description": "asc | desc (default).",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "zones": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "name": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          },
                          "site_url": {},
                          "traffic_back_url": {
                            "type": "string"
                          },
                          "postback_url": {
                            "type": "string"
                          },
                          "user_email": {
                            "type": "string",
                            "format": "email"
                          },
                          "manager_email": {},
                          "created_at": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        },
                        "has_more": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "zones": [
                    {
                      "id": "550e8400-e29b-41d4-a716-446655440000",
                      "name": "RichAds Push",
                      "status": "active",
                      "site_url": null,
                      "traffic_back_url": "https://richads.example/fallback",
                      "postback_url": "https://richads.example/pb?click_id={source_click_id}&payout={payout}",
                      "user_email": "publisher@example.com",
                      "manager_email": null,
                      "created_at": 1753747200000
                    }
                  ],
                  "pagination": {
                    "total": 3,
                    "limit": 20,
                    "offset": 0,
                    "has_more": false
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#zones-list"
        ],
        "x-affset-permission": "read"
      },
      "post": {
        "operationId": "zonesCreate",
        "summary": "Create a zone",
        "description": "- Zones are always created active.\n- Creating a zone counts against the plan’s zone limit and can return 402.\n- Affset doesn’t require {source_click_id} (or the legacy {sub_id}) in postback_url, but a postback without it can’t be matched back to a click — you’ll get a warning, not a rejection.\n- traffic_back_url is only the /serve fallback when no campaign can deliver — it is not a conversion postback.\n\nDocs: https://affset.com/docs#zones-create",
        "tags": [
          "Zones"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/XNamespace"
          }
        ],
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "status": {
                      "type": "string"
                    },
                    "created_at": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "id": "550e8400-e29b-41d4-a716-446655440000",
                  "status": "active",
                  "created_at": 1753747200000
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "402": {
            "$ref": "#/components/responses/Error402"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#zones-create"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Zone name."
                  },
                  "site_url": {
                    "type": "string",
                    "description": "Must be http(s) if present."
                  },
                  "traffic_back_url": {
                    "type": "string",
                    "description": "Where /serve sends traffic when there’s no eligible campaign. Must be http(s) if present."
                  },
                  "postback_url": {
                    "type": "string",
                    "description": "Affiliate passback — Affset GETs this on conversion. Macros: {payout}, {source_click_id} (alias {sub_id}), {sub1}…{sub5}. Must be http(s) if present."
                  },
                  "user_email": {
                    "type": "string",
                    "description": "Publishers can only create for themselves. Owner/manager/publisher_manager may assign any publisher."
                  }
                },
                "required": [
                  "name"
                ]
              },
              "example": {
                "name": "RichAds Push",
                "postback_url": "https://richads.example/pb?click_id={source_click_id}&payout={payout}"
              }
            }
          }
        },
        "x-affset-permission": "write"
      }
    },
    "/api/zones/{zone_id}": {
      "get": {
        "operationId": "zonesGet",
        "summary": "Get one zone",
        "description": "Docs: https://affset.com/docs#zones-get",
        "tags": [
          "Zones"
        ],
        "parameters": [
          {
            "name": "zone_id",
            "in": "path",
            "required": true,
            "description": "Zone id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "550e8400-e29b-41d4-a716-446655440000"
          },
          {
            "$ref": "#/components/parameters/XNamespace"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "name": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    },
                    "postback_url": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "id": "550e8400-e29b-41d4-a716-446655440000",
                  "name": "RichAds Push",
                  "status": "active",
                  "postback_url": "https://richads.example/pb?click_id={source_click_id}&payout={payout}"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#zones-get"
        ],
        "x-affset-permission": "read"
      },
      "put": {
        "operationId": "zonesUpdate",
        "summary": "Update a zone (partial)",
        "description": "Docs: https://affset.com/docs#zones-update",
        "tags": [
          "Zones"
        ],
        "parameters": [
          {
            "name": "zone_id",
            "in": "path",
            "required": true,
            "description": "Zone id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "550e8400-e29b-41d4-a716-446655440000"
          },
          {
            "$ref": "#/components/parameters/XNamespace"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "updated_at": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "id": "550e8400-e29b-41d4-a716-446655440000",
                  "updated_at": 1753887600000
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#zones-update"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "New zone name."
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "active",
                      "inactive"
                    ],
                    "description": "Unlike campaigns, there’s no archived state for zones."
                  },
                  "site_url": {
                    "type": "string",
                    "nullable": true,
                    "description": "Send null to clear a URL."
                  },
                  "traffic_back_url": {
                    "type": "string",
                    "nullable": true,
                    "description": "Send null to clear a URL."
                  },
                  "postback_url": {
                    "type": "string",
                    "nullable": true,
                    "description": "Send null to clear a URL."
                  }
                },
                "minProperties": 1
              },
              "example": {
                "postback_url": null
              }
            }
          }
        },
        "x-affset-permission": "write"
      },
      "delete": {
        "operationId": "zonesDelete",
        "summary": "Delete a zone",
        "description": "Docs: https://affset.com/docs#zones-delete",
        "tags": [
          "Zones"
        ],
        "parameters": [
          {
            "name": "zone_id",
            "in": "path",
            "required": true,
            "description": "Zone id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "550e8400-e29b-41d4-a716-446655440000"
          },
          {
            "$ref": "#/components/parameters/XNamespace"
          }
        ],
        "responses": {
          "204": {
            "description": "No Content"
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#zones-delete"
        ],
        "x-affset-permission": "write"
      }
    },
    "/api/api-keys": {
      "get": {
        "operationId": "teamList",
        "summary": "List team members or machine keys",
        "description": "Access: Owner and manager see the whole tenant. publisher_manager / advertiser_manager see only their own assigned publishers/advertisers.\n\n- ⚠️ Unlike the dashboard’s Team page, this response includes each member’s live bearer token in plaintext. If you’re building a UI, log, or support tool on top of this endpoint, redact token before you display or store it anywhere.\n- Returns a bare array — no pagination envelope, unlike every other list endpoint.\n\nDocs: https://affset.com/docs#team-list",
        "tags": [
          "Team"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/XNamespace"
          },
          {
            "name": "type",
            "in": "query",
            "required": true,
            "description": "user = people (dashboard/API logins). api-key = machine keys with no owning person.",
            "schema": {
              "type": "string",
              "enum": [
                "user",
                "api-key"
              ]
            },
            "example": "user"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "token": {
                        "type": "string"
                      },
                      "namespace": {
                        "type": "string"
                      },
                      "user_id": {
                        "type": "string"
                      },
                      "email": {
                        "type": "string",
                        "format": "email"
                      },
                      "role": {
                        "type": "string"
                      },
                      "created_at": {
                        "type": "integer"
                      },
                      "permissions": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      }
                    }
                  }
                },
                "example": [
                  {
                    "token": "<redacted — the live response contains the bearer token>",
                    "namespace": "acme-media",
                    "user_id": "usr_8f2a1c",
                    "email": "buyer@example.com",
                    "role": "advertiser",
                    "created_at": 1753747200000,
                    "permissions": [
                      "read",
                      "write"
                    ]
                  }
                ]
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#team-list"
        ],
        "x-affset-permission": "read"
      },
      "post": {
        "operationId": "teamCreate",
        "summary": "Invite a team member / issue a machine key",
        "description": "Access: Owner and manager can create any role. publisher_manager can only create publisher (assigned to themselves). advertiser_manager can only create advertiser.\n\n- The plaintext token is returned here, by rotate, and by the list endpoint to permitted roles. Treat all three responses as secrets and avoid logging them.\n- This creates the key directly, like the dashboard’s \"Add team member\" — it does not send an invite email. Hand the token to the person yourself, over a channel you trust.\n- type=user is gated by the seat limit (402) and requires a verified tenant email address (403, code EMAIL_VERIFICATION_REQUIRED). type=api-key uses the separate machine-key plan limit.\n\nDocs: https://affset.com/docs#team-create",
        "tags": [
          "Team"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/XNamespace"
          },
          {
            "name": "type",
            "in": "query",
            "required": true,
            "description": "Use user for a person with an email, or api-key for a machine credential.",
            "schema": {
              "type": "string",
              "enum": [
                "user",
                "api-key"
              ]
            },
            "example": "user"
          }
        ],
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": {
                      "type": "string"
                    },
                    "namespace": {
                      "type": "string"
                    },
                    "email": {
                      "type": "string",
                      "format": "email"
                    },
                    "role": {
                      "type": "string"
                    },
                    "permissions": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "created_at": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "token": "<new live bearer token>",
                  "namespace": "acme-media",
                  "email": "sarah@offer.com",
                  "role": "publisher",
                  "permissions": [
                    "read",
                    "write"
                  ],
                  "created_at": 1753747200000
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "402": {
            "$ref": "#/components/responses/Error402"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#team-create"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "Required when type=user, ignored for api-key."
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "owner",
                      "manager",
                      "advertiser",
                      "advertiser_manager",
                      "publisher",
                      "publisher_manager"
                    ],
                    "description": "owner | manager | publisher | advertiser | advertiser_manager | publisher_manager."
                  },
                  "permissions": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "read",
                        "write"
                      ]
                    },
                    "default": [
                      "read",
                      "write"
                    ],
                    "description": "Any of read, write. Default [\"read\",\"write\"]."
                  },
                  "manager_email": {
                    "type": "string",
                    "description": "Only valid for type=user when role is publisher or advertiser."
                  },
                  "expires_at": {
                    "type": "integer",
                    "description": "Optional future expiration time. Unix time in milliseconds."
                  }
                },
                "required": [
                  "role"
                ]
              },
              "example": {
                "email": "sarah@offer.com",
                "role": "publisher"
              }
            }
          }
        },
        "x-affset-permission": "write"
      },
      "delete": {
        "operationId": "teamRevokeRemoveTerminate",
        "summary": "Revoke a team member or key / Permanently remove a team member or key / Close the account",
        "description": "**Revoke a team member or key**\n\n- Sets the key’s expiry to now. For a user key, it also pauses campaigns and deactivates zones owned by that email. History stays intact.\n\nDocs: https://affset.com/docs#team-revoke\n\n---\n\n**Permanently remove a team member or key**\n\n- Hard-deletes an expired key. For a user key, it also deletes campaigns and zones owned by that email; removing a machine key deletes only the credential. Revoke an active key first.\n\nDocs: https://affset.com/docs#team-remove\n\n---\n\n**Close the account**\n\nAccess: Owner only.\n\n- ⚠️ Deletes the entire tenant — every campaign, zone, team member and history record. Irreversible. This is account closure, not team management.\n\nDocs: https://affset.com/docs#team-terminate",
        "tags": [
          "Team"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/XNamespace"
          }
        ],
        "responses": {
          "204": {
            "description": "No Content"
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#team-revoke",
          "https://affset.com/docs#team-remove",
          "https://affset.com/docs#team-terminate"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "Bearer token to revoke."
                  },
                  "action": {
                    "type": "string",
                    "enum": [
                      "revoke",
                      "remove",
                      "terminate"
                    ],
                    "description": "One of: revoke, remove, terminate."
                  }
                },
                "required": [
                  "token",
                  "action"
                ]
              },
              "examples": {
                "team-revoke": {
                  "summary": "Revoke a team member or key",
                  "value": {
                    "token": "sk_live_...",
                    "action": "revoke"
                  }
                },
                "team-remove": {
                  "summary": "Permanently remove a team member or key",
                  "value": {
                    "token": "sk_live_...",
                    "action": "remove"
                  }
                },
                "team-terminate": {
                  "summary": "Close the account",
                  "value": {
                    "token": "sk_live_...",
                    "action": "terminate"
                  }
                }
              }
            }
          }
        },
        "x-affset-permission": "write"
      },
      "patch": {
        "operationId": "teamRotateSetManager",
        "summary": "Rotate a token / Reassign who manages this person",
        "description": "**Rotate a token**\n\n- Issues a new token for the same identity and invalidates the old one immediately.\n\nDocs: https://affset.com/docs#team-rotate\n\n---\n\n**Reassign who manages this person**\n\nDocs: https://affset.com/docs#team-set-manager",
        "tags": [
          "Team"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/XNamespace"
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Required for: Reassign who manages this person (type=user).",
            "schema": {
              "type": "string",
              "enum": [
                "user"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": {
                      "type": "string"
                    },
                    "role": {
                      "type": "string"
                    },
                    "permissions": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "user_id": {
                      "type": "string"
                    },
                    "manager_email": {
                      "type": "string",
                      "format": "email"
                    }
                  }
                },
                "examples": {
                  "team-rotate": {
                    "summary": "Rotate a token",
                    "value": {
                      "token": "<new live bearer token>",
                      "role": "publisher",
                      "permissions": [
                        "read",
                        "write"
                      ]
                    }
                  },
                  "team-set-manager": {
                    "summary": "Reassign who manages this person",
                    "value": {
                      "user_id": "usr_8f2a1c",
                      "manager_email": "manager@acme-media.com"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#team-rotate",
          "https://affset.com/docs#team-set-manager"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "Active bearer token to replace."
                  },
                  "action": {
                    "type": "string",
                    "enum": [
                      "rotate"
                    ],
                    "description": "Must be rotate."
                  },
                  "manager_email": {
                    "type": "string",
                    "nullable": true,
                    "description": "The publisher_manager/advertiser_manager they report to. null clears it."
                  }
                },
                "required": [
                  "token"
                ]
              },
              "examples": {
                "team-rotate": {
                  "summary": "Rotate a token",
                  "value": {
                    "token": "sk_live_...",
                    "action": "rotate"
                  }
                },
                "team-set-manager": {
                  "summary": "Reassign who manages this person",
                  "value": {
                    "token": "sk_live_...",
                    "manager_email": "manager@acme-media.com"
                  }
                }
              }
            }
          }
        },
        "x-affset-permission": "write"
      }
    },
    "/api/campaigns/{campaign_id}/payout_rules": {
      "get": {
        "operationId": "payoutList",
        "summary": "List a campaign’s payout rules",
        "description": "Docs: https://affset.com/docs#payout-list",
        "tags": [
          "Payout rules"
        ],
        "parameters": [
          {
            "name": "campaign_id",
            "in": "path",
            "required": true,
            "description": "Campaign id.",
            "schema": {
              "type": "integer"
            },
            "example": 42
          },
          {
            "$ref": "#/components/parameters/XNamespace"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "payout_rules": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "campaign_id": {
                            "type": "integer"
                          },
                          "zone_id": {
                            "type": "string",
                            "format": "uuid",
                            "nullable": true
                          },
                          "payout": {
                            "type": "number"
                          },
                          "created_at": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "payout_rules": [
                    {
                      "id": 12,
                      "campaign_id": 42,
                      "zone_id": null,
                      "payout": 2.5,
                      "created_at": 1753747200000
                    },
                    {
                      "id": 13,
                      "campaign_id": 42,
                      "zone_id": "550e8400-e29b-41d4-a716-446655440000",
                      "payout": 3,
                      "created_at": 1753747200000
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#payout-list"
        ],
        "x-affset-permission": "read"
      },
      "post": {
        "operationId": "payoutCreate",
        "summary": "Create a payout rule",
        "description": "- Resolution at conversion time: zone-specific rule, then the global rule, then $0.\n- 409 if a rule already exists for that exact campaign + zone (or campaign + global) — see \"Changing a payout\" below.\n\nDocs: https://affset.com/docs#payout-create",
        "tags": [
          "Payout rules"
        ],
        "parameters": [
          {
            "name": "campaign_id",
            "in": "path",
            "required": true,
            "description": "Campaign id.",
            "schema": {
              "type": "integer"
            },
            "example": 42
          },
          {
            "$ref": "#/components/parameters/XNamespace"
          }
        ],
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "campaign_id": {
                      "type": "integer"
                    },
                    "zone_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "payout": {
                      "type": "integer"
                    },
                    "created_at": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "id": 13,
                  "campaign_id": 42,
                  "zone_id": "550e8400-e29b-41d4-a716-446655440000",
                  "payout": 3,
                  "created_at": 1753747200000
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "409": {
            "$ref": "#/components/responses/Error409"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#payout-create"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "payout": {
                    "type": "number",
                    "minimum": 0.00001,
                    "maximum": 9999.99999,
                    "description": "0.00001–9999.99999."
                  },
                  "zone_id": {
                    "type": "string",
                    "description": "Omit for the global (fallback) rule."
                  }
                },
                "required": [
                  "payout"
                ]
              },
              "example": {
                "payout": 3,
                "zone_id": "550e8400-e29b-41d4-a716-446655440000"
              }
            }
          }
        },
        "x-affset-permission": "write"
      },
      "delete": {
        "operationId": "payoutDelete",
        "summary": "Delete a payout rule",
        "description": "Docs: https://affset.com/docs#payout-delete",
        "tags": [
          "Payout rules"
        ],
        "parameters": [
          {
            "name": "campaign_id",
            "in": "path",
            "required": true,
            "description": "Campaign id.",
            "schema": {
              "type": "integer"
            },
            "example": 42
          },
          {
            "$ref": "#/components/parameters/XNamespace"
          },
          {
            "name": "zone_id",
            "in": "query",
            "required": false,
            "description": "Omit to delete the global rule.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "No Content"
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#payout-delete"
        ],
        "x-affset-permission": "write"
      }
    },
    "/api/targeting-rule-types": {
      "get": {
        "operationId": "targetingCatalog",
        "summary": "Catalog of targeting rule types",
        "description": "- ⚠️ capping, weekdays and hours are accepted and stored, but /serve never evaluates them — writing them has no effect on delivery. Use unique_users for frequency capping instead.\n- geo, os and browser are matched exactly and case-sensitively. An unmatched whitelist value silently stops delivery rather than erroring anywhere.\n\nDocs: https://affset.com/docs#targeting-catalog",
        "tags": [
          "Targeting"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/XNamespace"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "targeting_rule_types": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          },
                          "description": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "targeting_rule_types": [
                    {
                      "id": 1,
                      "name": "geo",
                      "description": "Comma-separated ISO-3166 alpha-2 country codes, matched against the request’s country."
                    },
                    {
                      "id": 2,
                      "name": "device_type",
                      "description": "desktop, mobile or tablet, comma-separated."
                    },
                    {
                      "id": 3,
                      "name": "capping",
                      "description": "Stored only — not evaluated by /serve."
                    },
                    {
                      "id": 4,
                      "name": "zone_id",
                      "description": "Comma-separated zone IDs. This is how zone blacklists/whitelists work."
                    },
                    {
                      "id": 5,
                      "name": "os",
                      "description": "Comma-separated OS names, exact match, e.g. Android, iOS, Windows."
                    },
                    {
                      "id": 6,
                      "name": "browser",
                      "description": "Comma-separated browser names, exact match, e.g. Chrome, Safari."
                    },
                    {
                      "id": 7,
                      "name": "weekdays",
                      "description": "Stored only — not evaluated by /serve."
                    },
                    {
                      "id": 8,
                      "name": "hours",
                      "description": "Stored only — not evaluated by /serve."
                    },
                    {
                      "id": 9,
                      "name": "unique_users",
                      "description": "A single \"visits/hours\" value, e.g. 1/24 — frequency capping."
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#targeting-catalog"
        ],
        "x-affset-permission": "read"
      }
    },
    "/api/campaigns/{campaign_id}/targeting_rules": {
      "get": {
        "operationId": "targetingList",
        "summary": "List a campaign’s targeting rules",
        "description": "Docs: https://affset.com/docs#targeting-list",
        "tags": [
          "Targeting"
        ],
        "parameters": [
          {
            "name": "campaign_id",
            "in": "path",
            "required": true,
            "description": "Campaign id.",
            "schema": {
              "type": "integer"
            },
            "example": 42
          },
          {
            "$ref": "#/components/parameters/XNamespace"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "targeting_rules": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "targeting_rule_type_id": {
                            "type": "integer"
                          },
                          "targeting_method": {
                            "type": "string"
                          },
                          "rule": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "targeting_rules": [
                    {
                      "id": 501,
                      "targeting_rule_type_id": 1,
                      "targeting_method": "whitelist",
                      "rule": "BR,MX"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#targeting-list"
        ],
        "x-affset-permission": "read"
      },
      "post": {
        "operationId": "targetingSync",
        "summary": "Replace a campaign’s targeting rules",
        "description": "- ⚠️ This replaces the whole set for the campaign. Any existing rule whose id is left out of the body gets deleted. Always GET the current list first, then send it back with your change folded in — see the recipes below.\n\nDocs: https://affset.com/docs#targeting-sync",
        "tags": [
          "Targeting"
        ],
        "parameters": [
          {
            "name": "campaign_id",
            "in": "path",
            "required": true,
            "description": "Campaign id.",
            "schema": {
              "type": "integer"
            },
            "example": 42
          },
          {
            "$ref": "#/components/parameters/XNamespace"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "integer"
                      },
                      "targeting_rule_type_id": {
                        "type": "integer"
                      },
                      "targeting_method": {
                        "type": "string"
                      },
                      "rule": {
                        "type": "string"
                      }
                    }
                  }
                },
                "example": [
                  {
                    "id": 501,
                    "targeting_rule_type_id": 1,
                    "targeting_method": "whitelist",
                    "rule": "BR,MX"
                  },
                  {
                    "id": 502,
                    "targeting_rule_type_id": 4,
                    "targeting_method": "blacklist",
                    "rule": "550e8400-e29b-41d4-a716-446655440000"
                  }
                ]
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#targeting-sync"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TargetingRuleInput"
                },
                "description": "targeting_method is \"whitelist\" or \"blacklist\"."
              },
              "example": [
                {
                  "id": 501,
                  "targeting_rule_type_id": 1,
                  "targeting_method": "whitelist",
                  "rule": "BR,MX"
                },
                {
                  "targeting_rule_type_id": 4,
                  "targeting_method": "blacklist",
                  "rule": "550e8400-e29b-41d4-a716-446655440000"
                }
              ]
            }
          }
        },
        "x-affset-permission": "write"
      }
    },
    "/api/conversions": {
      "get": {
        "operationId": "conversionsList",
        "summary": "List conversions",
        "description": "- ad_event_id and click_id are returned as strings — they’re 64-bit IDs that don’t fit exactly in a JS number.\n- No campaign, zone or date filters — just pagination and sort. This is the record-level audit trail; use Stats for aggregated, filterable reporting.\n- payload is a JSON-encoded string of the conversion pixel’s query parameters (except click_id), and may also include postback status fields. Anyone who can fire a pixel controls those values — treat them as untrusted data if you feed them into anything automated.\n- Same role-based redaction as Stats: publisher-side roles don’t see spend, advertiser-side roles don’t see payout.\n\nDocs: https://affset.com/docs#conversions-list",
        "tags": [
          "Conversions"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/XNamespace"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "1–100, default 20.",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            },
            "example": 5
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Default 0.",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "created_at (default) | ad_event_id | click_id.",
            "schema": {
              "type": "string",
              "enum": [
                "created_at",
                "ad_event_id",
                "click_id"
              ],
              "default": "created_at"
            }
          },
          {
            "name": "order",
            "in": "query",
            "required": false,
            "description": "asc | desc (default).",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "ad_event_id": {
                            "type": "string"
                          },
                          "click_id": {
                            "type": "string"
                          },
                          "payload": {
                            "type": "string"
                          },
                          "spend": {
                            "type": "integer"
                          },
                          "payout": {
                            "type": "integer"
                          },
                          "source_click_id": {
                            "type": "string"
                          },
                          "sub1": {
                            "type": "string"
                          },
                          "sub2": {},
                          "sub3": {},
                          "sub4": {},
                          "sub5": {},
                          "created_at": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        },
                        "has_more": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "conversions": [
                    {
                      "ad_event_id": "7291834650192837",
                      "click_id": "7291834612345678",
                      "payload": "{\"type\":\"deposit\",\"value\":\"49\"}",
                      "spend": 0,
                      "payout": 3,
                      "source_click_id": "abc123",
                      "sub1": "richads",
                      "sub2": null,
                      "sub3": null,
                      "sub4": null,
                      "sub5": null,
                      "created_at": 1753747200000
                    }
                  ],
                  "pagination": {
                    "total": 3,
                    "limit": 5,
                    "offset": 0,
                    "has_more": false
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#conversions-list"
        ],
        "x-affset-permission": "read"
      }
    },
    "/api/conversions/{ad_event_id}": {
      "delete": {
        "operationId": "conversionsDelete",
        "summary": "Delete a conversion",
        "description": "Access: Owner only.\n\n- Deletes the conversion row and its matching conversion event. The originating click remains in the event history.\n\nDocs: https://affset.com/docs#conversions-delete",
        "tags": [
          "Conversions"
        ],
        "parameters": [
          {
            "name": "ad_event_id",
            "in": "path",
            "required": true,
            "description": "Conversion event id — a 64-bit integer serialized as a string.",
            "schema": {
              "type": "string"
            },
            "example": "7291834650192837"
          },
          {
            "$ref": "#/components/parameters/XNamespace"
          }
        ],
        "responses": {
          "204": {
            "description": "No Content"
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#conversions-delete"
        ],
        "x-affset-permission": "write"
      }
    },
    "/serve/{zone_id}": {
      "get": {
        "operationId": "adServe",
        "summary": "Entry point you give a traffic source",
        "description": "- Picks one eligible campaign from the zone’s active, targeting-matched campaigns and redirects to /track/click for it.\n- No eligible campaign → redirects to the zone’s traffic_back_url if set, otherwise a plain-text 404.\n- source_click_id and sub1–sub5 carry forward to /track/click. cost is recorded on the /serve impression and deliberately not forwarded, so it is counted once; other query parameters are dropped.\n- Geo/device/OS/browser targeting is enforced here. It is not enforced on a direct /track/click link.\n\nDocs: https://affset.com/docs#ad-serve",
        "tags": [
          "Ad serving"
        ],
        "parameters": [
          {
            "name": "zone_id",
            "in": "path",
            "required": true,
            "description": "Zone id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "550e8400-e29b-41d4-a716-446655440000"
          },
          {
            "name": "source_click_id",
            "in": "query",
            "required": false,
            "description": "Your source’s click id. Legacy alias: sub_id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub1",
            "in": "query",
            "required": false,
            "description": "Passed through to the click and any conversions on it.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub2",
            "in": "query",
            "required": false,
            "description": "Passed through to the click and any conversions on it.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub3",
            "in": "query",
            "required": false,
            "description": "Passed through to the click and any conversions on it.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub4",
            "in": "query",
            "required": false,
            "description": "Passed through to the click and any conversions on it.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub5",
            "in": "query",
            "required": false,
            "description": "Passed through to the click and any conversions on it.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cost",
            "in": "query",
            "required": false,
            "description": "Media cost for this impression — see the note below. Plain unsigned decimal.",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK — HTML redirect page (JS + meta refresh) when the tenant’s redirect_method is html.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "Found — or 200 with an HTML redirect, per redirect_method",
            "headers": {
              "Location": {
                "description": "Redirect target.",
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "404": {
            "description": "Unknown zone, campaign or click — or nothing to serve and no traffic_back_url. Plain text, not JSON.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#ad-serve"
        ],
        "security": []
      }
    },
    "/track/click/{campaign_id}/{zone_id}": {
      "get": {
        "operationId": "adTrack",
        "summary": "Direct tracking link for one campaign",
        "description": "- Records the click and, for CPM campaigns, computes spend as rate/1000 — CPA campaigns accrue spend on conversion instead.\n- Not geo/targeting gated — only /serve enforces targeting.\n- Macros in redirect_url: {click_id}, {zone_id}, {source_click_id} (alias {aff_sub_id}), {sub1}…{sub5}. Values are percent-encoded on substitution. A macro with nothing to fill stays as literal text.\n\nDocs: https://affset.com/docs#ad-track",
        "tags": [
          "Ad serving"
        ],
        "parameters": [
          {
            "name": "campaign_id",
            "in": "path",
            "required": true,
            "description": "Campaign id.",
            "schema": {
              "type": "integer"
            },
            "example": 42
          },
          {
            "name": "zone_id",
            "in": "path",
            "required": true,
            "description": "Zone id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "550e8400-e29b-41d4-a716-446655440000"
          },
          {
            "name": "source_click_id",
            "in": "query",
            "required": false,
            "description": "Legacy alias: sub_id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub1",
            "in": "query",
            "required": false,
            "description": "Stored on the click and attributed to later conversions.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub2",
            "in": "query",
            "required": false,
            "description": "Stored on the click and attributed to later conversions.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub3",
            "in": "query",
            "required": false,
            "description": "Stored on the click and attributed to later conversions.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub4",
            "in": "query",
            "required": false,
            "description": "Stored on the click and attributed to later conversions.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub5",
            "in": "query",
            "required": false,
            "description": "Stored on the click and attributed to later conversions.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cost",
            "in": "query",
            "required": false,
            "description": "Only send this here or on /serve for a given stream, never both — sending it to both double-counts. Plain unsigned decimal.",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "302": {
            "description": "Found — to the campaign’s redirect_url, macros expanded",
            "headers": {
              "Location": {
                "description": "Redirect target.",
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "404": {
            "description": "Unknown zone, campaign or click — or nothing to serve and no traffic_back_url. Plain text, not JSON.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#ad-track"
        ],
        "security": []
      }
    },
    "/px/{click_id}": {
      "get": {
        "operationId": "adPixel",
        "summary": "Conversion pixel (into Affset)",
        "description": "- Send Accept: application/json for a JSON response. Other callers receive the 1×1 GIF used by browser pixels.\n- After a non-silent conversion, Affset GETs the zone’s postback_url (affiliate passback) with macros {payout}, {source_click_id} (alias {sub_id}), {sub1}…{sub5}. Skipped when silent or when the zone has no postback_url.\n- Payout comes from campaign payout rules (zone-specific → global → $0), not from a query param on /px.\n- Any other query parameter: Stored in the conversion payload (except click_id). source_click_id here cannot rewrite the click’s token used for the affiliate postback.\n\nDocs: https://affset.com/docs#ad-pixel",
        "tags": [
          "Ad serving"
        ],
        "parameters": [
          {
            "name": "click_id",
            "in": "path",
            "required": true,
            "description": "Affset click id — a 64-bit integer serialized as a string, from the {click_id} macro.",
            "schema": {
              "type": "string"
            },
            "example": "7291834612345678"
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Goal label. When the campaign has payout_goal_type set, only an exact type= match accrues spend/payout; others still record at $0.",
            "schema": {
              "type": "string"
            },
            "example": "deposit"
          },
          {
            "name": "sub1",
            "in": "query",
            "required": false,
            "description": "Fill empty slots from the click only — cannot overwrite values already stored on the click.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub2",
            "in": "query",
            "required": false,
            "description": "Fill empty slots from the click only — cannot overwrite values already stored on the click.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub3",
            "in": "query",
            "required": false,
            "description": "Fill empty slots from the click only — cannot overwrite values already stored on the click.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub4",
            "in": "query",
            "required": false,
            "description": "Fill empty slots from the click only — cannot overwrite values already stored on the click.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub5",
            "in": "query",
            "required": false,
            "description": "Fill empty slots from the click only — cannot overwrite values already stored on the click.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK — 1×1 GIF by default, or {\"status\":\"ok\"} for a JSON-flavored request",
            "content": {
              "image/gif": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "status": "ok"
                }
              }
            }
          },
          "404": {
            "description": "Unknown zone, campaign or click — or nothing to serve and no traffic_back_url. Plain text, not JSON.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#ad-pixel"
        ],
        "security": []
      }
    },
    "/px": {
      "get": {
        "operationId": "adPixelQuery",
        "summary": "Conversion pixel (into Affset) — query form",
        "description": "- Send Accept: application/json for a JSON response. Other callers receive the 1×1 GIF used by browser pixels.\n- After a non-silent conversion, Affset GETs the zone’s postback_url (affiliate passback) with macros {payout}, {source_click_id} (alias {sub_id}), {sub1}…{sub5}. Skipped when silent or when the zone has no postback_url.\n- Payout comes from campaign payout rules (zone-specific → global → $0), not from a query param on /px.\n- Any other query parameter: Stored in the conversion payload (except click_id). source_click_id here cannot rewrite the click’s token used for the affiliate postback.\n\nDocs: https://affset.com/docs#ad-pixel",
        "tags": [
          "Ad serving"
        ],
        "parameters": [
          {
            "name": "click_id",
            "in": "query",
            "required": true,
            "description": "Affset click id from the offer redirect’s {click_id}.",
            "schema": {
              "type": "string"
            },
            "example": "7291834612345678"
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Goal label. When the campaign has payout_goal_type set, only an exact type= match accrues spend/payout; others still record at $0.",
            "schema": {
              "type": "string"
            },
            "example": "deposit"
          },
          {
            "name": "sub1",
            "in": "query",
            "required": false,
            "description": "Fill empty slots from the click only — cannot overwrite values already stored on the click.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub2",
            "in": "query",
            "required": false,
            "description": "Fill empty slots from the click only — cannot overwrite values already stored on the click.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub3",
            "in": "query",
            "required": false,
            "description": "Fill empty slots from the click only — cannot overwrite values already stored on the click.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub4",
            "in": "query",
            "required": false,
            "description": "Fill empty slots from the click only — cannot overwrite values already stored on the click.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sub5",
            "in": "query",
            "required": false,
            "description": "Fill empty slots from the click only — cannot overwrite values already stored on the click.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK — 1×1 GIF by default, or {\"status\":\"ok\"} for a JSON-flavored request",
            "content": {
              "image/gif": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "status": "ok"
                }
              }
            }
          },
          "404": {
            "description": "Unknown zone, campaign or click — or nothing to serve and no traffic_back_url. Plain text, not JSON.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "x-affset-docs": [
          "https://affset.com/docs#ad-pixel"
        ],
        "security": []
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Tenant API key or session token, sent together with the X-Namespace header. Key permissions: read (GET) and write (POST, PUT, PATCH, DELETE) — see x-affset-permission on each operation. Issue least-privilege keys from the dashboard Team page or POST /api/api-keys?type=api-key."
      }
    },
    "parameters": {
      "XNamespace": {
        "name": "X-Namespace",
        "in": "header",
        "required": true,
        "description": "The tenant the bearer token belongs to. A token used with another namespace is rejected with 401.",
        "schema": {
          "type": "string"
        },
        "example": "acme-media"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable message."
          },
          "code": {
            "type": "string",
            "description": "Machine-readable code on some errors, e.g. EMAIL_VERIFICATION_REQUIRED or PLAN_LIMIT_REACHED."
          }
        }
      },
      "PlanLimitError": {
        "description": "Returned with HTTP 402 when an action would exceed the plan.",
        "type": "object",
        "required": [
          "error",
          "code",
          "dimension",
          "limit",
          "current",
          "plan_id"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable message."
          },
          "code": {
            "type": "string",
            "enum": [
              "PLAN_LIMIT_REACHED"
            ]
          },
          "dimension": {
            "type": "string",
            "description": "Which plan dimension is exhausted, e.g. campaigns, zones, team, api_keys, custom_api_domain."
          },
          "limit": {
            "type": "integer"
          },
          "current": {
            "type": "integer"
          },
          "plan_id": {
            "type": "string"
          },
          "min_plan_id": {
            "type": "string",
            "nullable": true,
            "description": "Cheapest plan that would allow the action."
          }
        },
        "example": {
          "error": "Plan limit reached for campaigns",
          "code": "PLAN_LIMIT_REACHED",
          "dimension": "campaigns",
          "limit": 10,
          "current": 10,
          "plan_id": "free",
          "min_plan_id": "starter"
        }
      },
      "TargetingRuleInput": {
        "type": "object",
        "required": [
          "targeting_rule_type_id",
          "targeting_method",
          "rule"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Existing rule id to keep; omit for a new rule."
          },
          "targeting_rule_type_id": {
            "type": "integer",
            "description": "From GET /api/targeting-rule-types."
          },
          "targeting_method": {
            "type": "string",
            "enum": [
              "whitelist",
              "blacklist"
            ]
          },
          "rule": {
            "type": "string",
            "description": "Comma-separated values as described for the rule type."
          }
        }
      }
    },
    "responses": {
      "Error400": {
        "description": "Bad Request — missing header, parameter, or body, or an invalid value.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error401": {
        "description": "Unauthorized — invalid or expired key, or X-Namespace doesn't match it.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error402": {
        "description": "Payment Required — you’re at a plan limit. See below.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PlanLimitError"
            }
          }
        }
      },
      "Error403": {
        "description": "Forbidden — your role or permissions don't allow this. Also used to mask ownership on writes.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error404": {
        "description": "Not Found — the resource doesn't exist, or isn't visible to your role.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error409": {
        "description": "Conflict — something with the same identity already exists.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error500": {
        "description": "Internal Server Error — something went wrong on our end.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "x-affset": {
    "docs": "https://affset.com/docs",
    "api_reference_markdown": "https://affset.com/api-reference.md",
    "api_reference_json": "https://affset.com/api-reference.json",
    "llms_txt": "https://affset.com/llms.txt",
    "permissions": {
      "read": {
        "methods": [
          "GET"
        ],
        "description": "GET endpoints — list and read resources, stats, and conversions."
      },
      "write": {
        "methods": [
          "POST",
          "PUT",
          "PATCH",
          "DELETE"
        ],
        "description": "POST, PUT, PATCH and DELETE — create, update, delete, rotate, revoke."
      }
    },
    "mcp": {
      "endpoint": "https://mcp.affset.com/mcp",
      "oauth_protected_resource_metadata": "https://mcp.affset.com/.well-known/oauth-protected-resource",
      "oauth_authorization_server_metadata": "https://oauth.affset.com/.well-known/oauth-authorization-server",
      "source": "https://github.com/affset/mcp",
      "scopes": {
        "read": {
          "permissions": [
            "read"
          ],
          "description": "Read-only tool set — every tool that writes is stripped from the session."
        },
        "full": {
          "permissions": [
            "read",
            "write"
          ],
          "description": "Every tool, backed by a read+write key. Only offered to roles that can write."
        }
      }
    }
  }
}
