{
  "openapi": "3.1.0",
  "info": {
    "title": "PlanVortex API",
    "description": "Schedule and publish on social networks, read comments and messages, and manage the files behind them.\n\nThis document is the union of the per-section specifications published at https://planvortex.com/documentation, and it is what the official client libraries generate their types from.\n\n**Classify errors by `code`, never by the HTTP status**: every domain error travels with HTTP 400.",
    "termsOfService": "https://planvortex.com/term",
    "contact": {
      "email": "contact@planvortex.com"
    },
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://api.planvortex.com/v1.0.0"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    },
    {
      "clientCredentials": []
    }
  ],
  "tags": [
    {
      "name": "accounts",
      "description": "Social networks accounts connected to the organization"
    },
    {
      "name": "ai_plans",
      "description": "AI-generated weekly publication plans"
    },
    {
      "name": "apps",
      "description": "Client apps: the credentials an integration authenticates with"
    },
    {
      "name": "authentication",
      "description": "Exchanging credentials for an access token"
    },
    {
      "name": "catalog",
      "description": "Supported networks, their capabilities and their limits"
    },
    {
      "name": "clients",
      "description": "Everything about the clients you have access"
    },
    {
      "name": "clients_roles",
      "description": "Client roles manager. Define new client roles and manage them"
    },
    {
      "name": "comments",
      "description": "The public conversation hanging off what you published — comments and reviews"
    },
    {
      "name": "contacts",
      "description": "People an organization talks to"
    },
    {
      "name": "dashboard",
      "description": "Aggregates of an organization and its children"
    },
    {
      "name": "files",
      "description": "Files uploaded to an organization resource"
    },
    {
      "name": "integrations",
      "description": "Connections between an organization and a third-party tool (Google Drive, RSS feeds)"
    },
    {
      "name": "message templates",
      "description": "Pre-approved templates, the only way to open a conversation on WhatsApp"
    },
    {
      "name": "messages",
      "description": "Conversations and messages of a connected account"
    },
    {
      "name": "organization_roles",
      "description": "Roles and user membership inside an organization"
    },
    {
      "name": "orgnaizations",
      "description": "Organizations management. Define new organizations and isolate resources between them"
    },
    {
      "name": "products",
      "description": "Commerce catalogues and products of a connected account"
    },
    {
      "name": "publications",
      "description": "Publications on social networks"
    },
    {
      "name": "webhooks",
      "description": "What PlanVortex posts to your app when a comment arrives"
    }
  ],
  "paths": {
    "/organizations/{id_organization}/connect_links": {
      "get": {
        "tags": [
          "accounts"
        ],
        "summary": "Get the connection links of every connectable network",
        "description": "The authorization URL of each network, so the user can connect an account to this organization.\n\n**A network that cannot produce a link simply does not appear.** That is a legitimate answer, not a failure: it is what happens with `discord` in an organization that has not saved its own bot credentials yet (see `PUT /organizations/{id_organization}/social_credentials/{social_network}`).\n\n**An app cannot call this.** Connecting a social account is an OAuth flow with a person in front of it, so this endpoint only accepts a user token or a temporal connect token; with app credentials it answers error 519. The way an integration does it is to issue a temporal connect token with `GET /organizations/{id_organization}/temporal_connect_token` and hand it to its end user.\n\n**Read `authorization`, not `link`.** Nine of the ten networks are `redirect` and you send the person to `link`. **WhatsApp is not a URL at all**: its sign-up is Meta's Embedded Signup, a popup you raise with the Facebook JavaScript SDK, so its `link` is an empty string and everything you need to open that popup travels in `authorization`. A client that loops over the list and redirects to `link` sends its user to its own page.",
        "operationId": "getConnectLinks",
        "x-planvortex-identity": [
          "current_user",
          "temporal_token"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountsSocialLinksList"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `523` | Invalid application |\n| `532` | `redirect_uri` is not one of the fronts registered on the server |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "social_network",
            "in": "query",
            "description": "Only these networks. Repeat the parameter for several. Omit it and every connectable network comes back.",
            "required": false,
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/SocialNetwork"
              }
            }
          },
          {
            "name": "redirect_uri",
            "in": "query",
            "description": "Which PlanVortex front the network sends the user back to, for a white-labelled deployment. It has to be one of the fronts the server has registered (`FRONT_URL_REDIRECT`) or the call answers error 532 — it is **not** your app's `redirect_urls`, and it cannot be a URL of yours: the networks only accept redirect URIs registered in their own app settings. Where your user goes once they have finished is the `redirect_uri` of `GET /organizations/{id_organization}/temporal_connect_token`.",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/organizations/{id_organization}/accounts": {
      "get": {
        "tags": [
          "accounts"
        ],
        "summary": "Get social accounts",
        "description": "Get social accounts list for the current organization paginated",
        "operationId": "getAccountList",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Number of records from which you will get roles (pagination)",
            "required": false,
            "explode": true,
            "schema": {
              "type": "integer",
              "default": "0"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "The limit of records will be retrieved (pagination)",
            "required": false,
            "explode": true,
            "schema": {
              "type": "integer",
              "default": "10"
            }
          },
          {
            "name": "name",
            "in": "query",
            "description": "Free-text search over the account's name and username.",
            "required": false,
            "explode": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "accounts",
            "in": "query",
            "description": "Only these accounts, by identifier. Repeat the parameter for more than one.",
            "required": false,
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "social_network",
            "in": "query",
            "description": "Only accounts of these networks. Repeat the parameter for more than one.",
            "required": false,
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/SocialNetwork"
              }
            }
          },
          {
            "name": "capability",
            "in": "query",
            "description": "Only accounts whose network can do this. It is the same matrix `GET /social_capabilities` publishes, applied server-side: the way to ask for 'the accounts I can publish with' without keeping your own table of which network does what.",
            "required": false,
            "explode": true,
            "schema": {
              "type": "string",
              "enum": [
                "publications",
                "messages",
                "products",
                "webhooks",
                "persistent_menu",
                "comments"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountsAccountList"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/organizations/{id_organization}/accounts/{id_account}": {
      "get": {
        "tags": [
          "accounts"
        ],
        "summary": "Get social accounts by identifier",
        "description": "Get social account by the received identifier",
        "operationId": "getAccount",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountsAccountOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `701` | Account doesn't exists or invalid |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "parameters": [
          {
            "name": "id_account",
            "in": "path",
            "description": "Account identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "put": {
        "tags": [
          "accounts"
        ],
        "summary": "Update social accounts by identifier",
        "description": "Update social account by the received identifier",
        "operationId": "updateAccount",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization the account belongs to",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_account",
            "in": "path",
            "description": "Connected social account identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountsAccountOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `701` | IAccount doesn't exists or invalid |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Only `name` is read. Any other property is ignored.",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Display name for the connected account inside PlanVortex. It does not rename anything on the social network. If omitted, the current name is kept."
                  }
                }
              },
              "examples": {
                "rename": {
                  "summary": "Rename the connected account",
                  "value": {
                    "name": "Client X - Instagram"
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "accounts"
        ],
        "summary": "Delete social accounts by identifier",
        "description": "Delete social account by the received identifier",
        "operationId": "deleteAccount",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `701` | Account doesn't exists or invalid |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "parameters": [
          {
            "name": "id_account",
            "in": "path",
            "description": "Account identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/organizations/{id_organization}/accounts/{id_account}/metric_list": {
      "get": {
        "tags": [
          "accounts"
        ],
        "summary": "Retrieve a list of possible metrics",
        "description": "Retrieve a list of possible metrics",
        "operationId": "getMetricList",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountsMetricList"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `701` | Account doesn't exists or invalid |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "parameters": [
          {
            "name": "id_account",
            "in": "path",
            "description": "Account identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/organizations/{id_organization}/accounts/{id_account}/metrics": {
      "get": {
        "tags": [
          "accounts"
        ],
        "summary": "Retrieve a list of possible metrics",
        "description": "Retrieve a list of possible metrics",
        "operationId": "getAccountStats",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountsMetricModel"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `701` | Account doesn't exists or invalid |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "parameters": [
          {
            "name": "id_account",
            "in": "path",
            "description": "Account identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from_date",
            "in": "query",
            "description": "Start of the range. Defaults to a day before `to_date`.",
            "required": false,
            "explode": true,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to_date",
            "in": "query",
            "description": "End of the range. Defaults to now.",
            "required": false,
            "explode": true,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "names",
            "in": "query",
            "description": "Only these metrics, by their RAW name — the ones `GET .../metric_list` returns. Repeat the parameter for more than one. Without it, every measured metric comes back.",
            "required": false,
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        ]
      }
    },
    "/organizations/{id_organization}/account-connect/{social_network}": {
      "get": {
        "tags": [
          "accounts"
        ],
        "summary": "Complete the connection of a social account",
        "description": "The endpoint the social network sends the user back to after they authorize. It turns the network's callback into one or more PlanVortex accounts.\n\n**You do not build this URL, the network does.** It is the `redirect_uri` inside the link that `GET /organizations/{id_organization}/connect_links` handed out, so the query parameters are whatever the network appends — typically `code` and `state`. Pass them through untouched.\n\n**One authorization can produce several accounts.** A Facebook user with four pages ends up with four; a Discord authorization produces the channel that was picked.\n\n**It answers 200 even when it fails.** The result carries `errorCode` and `errorMsg` instead of an error body, because the browser lands here from a redirect and a raw 400 would be a broken page. Check `errorCode`: empty means everything went well.\n\n**An app cannot call this** — it needs a user token or a temporal connect token (error 519 otherwise). See `GET /organizations/{id_organization}/temporal_connect_token`.\n\n**A temporal connect token is spent here.** Once this call succeeds, that token cannot connect anything else and answers error 543; the `enable` calls that finish the same connection still work until it expires. And if the token was issued for one network, calling this for another answers error 544.",
        "operationId": "connectAccount",
        "x-planvortex-identity": [
          "current_user",
          "temporal_token"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "social_network",
            "in": "path",
            "required": true,
            "description": "The network being connected.",
            "schema": {
              "$ref": "#/components/schemas/SocialNetwork"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The result of the connection. **Read `errorCode` before trusting `accounts`.**",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "accounts": {
                      "type": "array",
                      "description": "The accounts that were connected. Can be empty when the authorization produced nothing usable.",
                      "items": {
                        "$ref": "#/components/schemas/Account"
                      }
                    },
                    "errorCode": {
                      "type": "string",
                      "description": "PlanVortex error code as a string, or an empty string when it all went well. **It travels in a 200.**"
                    },
                    "errorMsg": {
                      "type": "string",
                      "description": "The matching message, or an empty string."
                    },
                    "redirect_uri": {
                      "type": "string",
                      "description": "Where to send the user next. Only present when the call was made with a temporal connect token that carried one."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `519` | This endpoint does not accept app credentials |\n| `543` | This temporal connect token has already connected an account. Issue a new one |\n| `544` | This temporal connect token was issued for a different social network |\n| `1101` | Invalid organization, or a temporal token for a different one |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/organizations/{id_organization}/accounts/{id_account}/enable": {
      "post": {
        "tags": [
          "accounts"
        ],
        "summary": "Re-enable a disconnected account",
        "description": "Brings back an account that was removed, without going through the authorization again — as long as its stored token still works.\n\nIt takes a **slot of the plan**, so it fails with error 706 when the organization is already at its account limit. It also turns the network's webhooks back on, on any plan but the free one.",
        "operationId": "enableAccount",
        "x-planvortex-identity": [
          "current_user",
          "temporal_token"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_account",
            "in": "path",
            "description": "Connected account identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Enabled. When the call came from a temporal connect token with a `redirect_uri`, the answer is that URL instead of `{success: true}`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "redirect_uri": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `700` | The account has no usable token: it has to be connected again |\n| `706` | The organization has no account slots left in its plan |\n| `519` | This endpoint does not accept app credentials |\n| `544` | This temporal connect token was issued for a different social network |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/organizations/{id_organization}/accounts/{id_account}/persistent_menu": {
      "get": {
        "tags": [
          "accounts"
        ],
        "summary": "Read the account's persistent menu",
        "description": "The fixed menu shown inside the network's chat. **Only Meta messaging accounts have one** — ask `persistent_menu` in `GET /social_capabilities`; anywhere else this answers error 710.\n\nIt is read live from the network.",
        "operationId": "getPersistentMenu",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_account",
            "in": "path",
            "description": "Connected account identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The menu",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "persistent_menu": {
                      "$ref": "#/components/schemas/AccountsPersistentMenu"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `710` | This network has no persistent menu |\n| `1101` | Invalid organization |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      },
      "post": {
        "tags": [
          "accounts"
        ],
        "summary": "Replace the account's persistent menu",
        "description": "**Replaces the whole menu.** There is no partial update: what you send is what the chat shows, and an empty array removes it.\n\nOne entry per locale. `default` is the fallback and has to be there.",
        "operationId": "setPersistentMenu",
        "x-planvortex-identity": [
          "current_user",
          "current_app",
          "temporal_token"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_account",
            "in": "path",
            "description": "Connected account identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "persistent_menu": {
                    "$ref": "#/components/schemas/AccountsPersistentMenu"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The menu as the network stored it",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "persistent_menu": {
                      "$ref": "#/components/schemas/AccountsPersistentMenu"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `710` | This network has no persistent menu |\n| `1101` | Invalid organization |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/clients/{id_client}/organizations/{id_organization}/ai_plans": {
      "post": {
        "tags": [
          "ai_plans"
        ],
        "summary": "Create an AI publication plan",
        "description": "Create a weekly AI publication plan. The plan is queued in state 'pending' and generated asynchronously by the generate-ai-plans job; poll GET by id while state is pending or generating. Validations at creation: the client plan allows AI (artificial_inteligence), the accounts belong to the organization, there are enough AI credits for the deterministic base cost (orchestration + target texts), and there is room in the organization's monthly publication limit. Returns the created plan together with the deterministic cost estimate. Requires the ai_plans:create permission.",
        "operationId": "addAiPlan",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AiPlansidClient"
          },
          {
            "$ref": "#/components/parameters/AiPlansidOrganization"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AiPlansAiPlanCreateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Plan created and queued. estimated_cost is the deterministic upper bound in AI credits; the actual credits_spent may be lower if the model produces fewer publications/images.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiPlansAiPlanCreateResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `806` | Invalid upload identifier. One of the gallery_uploads does not belong to the organization. |\n| `924` | Max publications reached per month. There is no room in the organization's monthly publication limit for the generated posts. |\n| `941` | AI credits exhausted for this month. The base cost of the plan exceeds the remaining AI credits. The response data carries { used, limit, cost }. |\n| `2101` | This functionality requires a plan with artificial intelligence (available from the basic plan). |\n| `2103` | All accounts must belong to the organization. |\n| `2105` | AI plan requires at least one account. |\n| `2106` | Invalid AI plan options (missing prompt, invalid timezone, invalid week_start, invalid max_images or invalid publish_days). |\n| `2108` | The selected publish days leave no available slot in the plan week: every slot of the chosen days is already in the past. Pick more days or a later week_start. The response data carries { publish_days, week_start, timezone }. |\n| `521` | Invalid client. The identifier is invalid or doesn't exist |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      },
      "get": {
        "tags": [
          "ai_plans"
        ],
        "summary": "List AI publication plans",
        "description": "Return the organization's AI plans ordered by creation date (desc), paginated. Requires the ai_plans:read permission.\n\n**Cancelled plans are not listed.** Deleting a plan sets it to `cancelled` rather than removing it, and this listing filters those out — so a plan you deleted simply stops appearing, while `GET` by id still returns it.\n\nWithout `limit` the whole list comes back.",
        "operationId": "getAiPlans",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AiPlansidClient"
          },
          {
            "$ref": "#/components/parameters/AiPlansidOrganization"
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Number of records to skip (pagination)",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of records to return (pagination)",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiPlansAiPlanList"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `521` | Invalid client. The identifier is invalid or doesn't exist |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/clients/{id_client}/organizations/{id_organization}/ai_plans/{id_ai_plan}": {
      "get": {
        "tags": [
          "ai_plans"
        ],
        "summary": "Get an AI publication plan by id",
        "description": "Return a single AI plan with its generated publications populated. This is the endpoint clients poll while state is pending or generating to detect when generation finishes. Requires the ai_plans:read permission.",
        "operationId": "getAiPlan",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AiPlansidClient"
          },
          {
            "$ref": "#/components/parameters/AiPlansidOrganization"
          },
          {
            "$ref": "#/components/parameters/AiPlansidAiPlan"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiPlansAiPlanOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `2100` | AI plan doesn't exist. The identifier is invalid or doesn't exist. |\n| `521` | Invalid client. The identifier is invalid or doesn't exist |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      },
      "delete": {
        "tags": [
          "ai_plans"
        ],
        "summary": "Cancel / discard an AI publication plan",
        "description": "Discard the plan and delete its generated draft publications. AI credits already spent on generation are NOT refunded (same criterion as X: if the provider charged, we charge). Requires the ai_plans:delete permission.",
        "operationId": "deleteAiPlan",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AiPlansidClient"
          },
          {
            "$ref": "#/components/parameters/AiPlansidOrganization"
          },
          {
            "$ref": "#/components/parameters/AiPlansidAiPlan"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `2100` | AI plan doesn't exist. The identifier is invalid or doesn't exist. |\n| `2102` | Invalid AI plan state for this operation. |\n| `521` | Invalid client. The identifier is invalid or doesn't exist |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/clients/{id_client}/organizations/{id_organization}/ai_plans/{id_ai_plan}/validate": {
      "post": {
        "tags": [
          "ai_plans"
        ],
        "summary": "Validate an AI publication plan",
        "description": "Validate a generated plan: its draft publications without errors are re-validated and moved to 'ready', and the plan moves to 'validated'. From that point the normal publish-pending flow publishes them at their publish_date. Only valid from state 'generated'. Requires the ai_plans:update permission.",
        "operationId": "validateAiPlan",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AiPlansidClient"
          },
          {
            "$ref": "#/components/parameters/AiPlansidOrganization"
          },
          {
            "$ref": "#/components/parameters/AiPlansidAiPlan"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation. Returns the plan (state validated) with its publications populated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiPlansAiPlanOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `2100` | AI plan doesn't exist. The identifier is invalid or doesn't exist. |\n| `2102` | Invalid AI plan state for this operation. Only a plan in state 'generated' can be validated. |\n| `521` | Invalid client. The identifier is invalid or doesn't exist |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/clients/{id_client}/organizations/{id_organization}/ai_plans/{id_ai_plan}/retry": {
      "post": {
        "tags": [
          "ai_plans"
        ],
        "summary": "Retry a failed AI publication plan",
        "description": "Re-queue a failed plan: it goes back to state 'pending' (attempts and error reset) and the background job regenerates it with the SAME data (prompt, accounts, options). Poll the plan while state is pending or generating, as after creation. Only valid from state 'failed'. Already-spent credits are NOT refunded and a new generation spends again; drafts left over from the failed attempt are discarded by the generation itself. If the failure was a business error (no AI credits, no monthly publication slots), retrying will fail the same way. Requires the ai_plans:update permission.",
        "operationId": "retryAiPlan",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AiPlansidClient"
          },
          {
            "$ref": "#/components/parameters/AiPlansidOrganization"
          },
          {
            "$ref": "#/components/parameters/AiPlansidAiPlan"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation. Returns the plan back in state 'pending'.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiPlansAiPlanOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `2100` | AI plan doesn't exist. The identifier is invalid or doesn't exist. |\n| `2102` | Invalid AI plan state for this operation. Only a plan in state 'failed' can be retried. |\n| `521` | Invalid client. The identifier is invalid or doesn't exist |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/clients/{id_client}/organizations/{id_organization}/ai_plans/{id_ai_plan}/publications/{id_publication}/regenerate": {
      "post": {
        "tags": [
          "ai_plans"
        ],
        "summary": "Regenerate the text or the image of one publication of the plan",
        "description": "Asks the model for another version of a single publication, for when the generated one does not work.\n\n**It costs credits, and they are charged per use.** The balance is checked before the call and the real cost is charged afterwards, accumulating in the plan's `credits_spent`. As a reference: regenerating a text is billed around 2 credits and an image around 70, but what is actually charged is the provider's real cost.\n\nIt only works **while the plan is under review** (state `generated`) and on its drafts: a publication already validated or published is not a draft any more.",
        "operationId": "regenerateAiPlanPublication",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_client",
            "in": "path",
            "description": "Client identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_ai_plan",
            "in": "path",
            "description": "AI plan identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_publication",
            "in": "path",
            "description": "Identifier of the plan's publication to regenerate",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "target"
                ],
                "properties": {
                  "target": {
                    "type": "string",
                    "description": "What to regenerate. `image` needs the plan to have been created with images allowed.",
                    "enum": [
                      "text",
                      "image"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The regenerated publication and what the plan has spent so far",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "publication": {
                      "$ref": "#/components/schemas/Publication"
                    },
                    "credits_spent": {
                      "type": "integer",
                      "description": "AI credits this plan has spent in total, this regeneration included."
                    }
                  },
                  "required": [
                    "publication",
                    "credits_spent"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `2102` | The plan is not under review: only a `generated` plan can be regenerated |\n| `2106` | Invalid `target`: it has to be `text` or `image` |\n| `935` | That publication does not belong to this plan, or is no longer a draft |\n| `941` | AI credits exhausted for this month |\n| `2101` | This functionality requires a plan with artificial intelligence |\n| `521` | Invalid client. The identifier is invalid or doesn't exist |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/oauth/token": {
      "post": {
        "tags": [
          "authentication"
        ],
        "summary": "Exchange client credentials for an access token",
        "description": "The way an integration authenticates. Send the app's `client_id` and `client_secret` and get back a bearer token to put in `Authorization` on every other call.\n\n**This endpoint is different from the rest of the API in three ways, and all three matter:**\n\n1. **It takes no token** — it is the one that gives you one. It is also the only endpoint with no authentication at all.\n2. **It accepts `application/x-www-form-urlencoded`**, which is what OAuth 2 says and what a generic OAuth client sends. JSON works too.\n3. **Its errors are OAuth 2 errors** — `{error, error_description}` with a 400 or a 401 — and *not* the `{code, message, data}` of everywhere else. This is the single exception in the whole API, and it exists so a generic OAuth client understands the answer.\n\nCredentials travel either in the body (`client_secret_post`) or in `Authorization: Basic base64(client_id:client_secret)` (`client_secret_basic`). Both are standard and both are accepted.\n\n**Cache the token until it expires.** The endpoint is rate limited per `client_id` and IP — 30 attempts a minute, and 10 *failures* in five minutes — so asking for a fresh token on every call will eventually get a 429 with a `Retry-After`. A correct integration asks once and refreshes shortly before `expires_in`.\n\nWhat comes back is an access token and nothing else: **no refresh token and no id token**. When it expires, ask again.",
        "operationId": "issueToken",
        "security": [],
        "x-planvortex-identity": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/AppsTokenRequest"
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AppsTokenRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The access token. The response is never cached (`Cache-Control: no-store`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppsTokenResponse"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` when `client_id` or `client_secret` is missing, `unsupported_grant_type` when `grant_type` is not `client_credentials`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppsOAuthError"
                }
              }
            }
          },
          "401": {
            "description": "`invalid_client`: the credentials are not valid, or the app no longer exists. **The message does not say which**, on purpose — telling the two apart is exactly what somebody enumerating clients wants to know.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppsOAuthError"
                }
              }
            }
          },
          "429": {
            "description": "`slow_down`: too many attempts, or too many failures. The `Retry-After` header says how many seconds to wait.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before trying again.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppsOAuthError"
                }
              }
            }
          }
        }
      }
    },
    "/clients/{id_client}/apps": {
      "get": {
        "tags": [
          "apps"
        ],
        "summary": "List the client's apps",
        "description": "The apps of a client. Secrets never travel here: to read one, ask for it explicitly with the `secret` endpoint.\n\n**Needs a USER token, not an app token.** With client credentials it answers `ERROR_CODE_512`. It also needs the **Custom plan** (`ERROR_CODE_542`): apps are what that plan adds, and they are the door to this whole API.",
        "operationId": "getClientApps",
        "x-planvortex-identity": [
          "current_user"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AppsidClient"
          },
          {
            "$ref": "#/components/parameters/Appsoffset"
          },
          {
            "$ref": "#/components/parameters/Appslimit"
          }
        ],
        "responses": {
          "200": {
            "description": "Apps",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "client_apps": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AppsClientApp"
                      }
                    },
                    "total": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "client_apps",
                    "total"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/AppsError"
          }
        }
      },
      "post": {
        "tags": [
          "apps"
        ],
        "summary": "Create an app",
        "description": "Creates the app and its credentials.\n\n**A client can have one app.** Creating a second answers error 536. If you need to rotate credentials, update the existing app rather than creating another.\n\nEvery URL is validated: a bad entry in `allowed_domains` answers 531, in `redirect_urls` 532, and a bad `webhook_url` answers 535.\n\n**The secret is not in the response.** Read it with `GET /clients/{id_client}/apps/{id_app}/secret`.\n\n**A client can only have ONE app.** Creating a second one is rejected with `ERROR_CODE_536`: delete or update the one that exists. The listing is paginated because every listing in this API is, not because there can be more than one.\n\n**Needs a USER token, not an app token.** With client credentials it answers `ERROR_CODE_512`. It also needs the **Custom plan** (`ERROR_CODE_542`): apps are what that plan adds, and they are the door to this whole API.",
        "operationId": "createClientApp",
        "x-planvortex-identity": [
          "current_user"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AppsidClient"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AppsClientAppInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created app",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "client_app": {
                      "$ref": "#/components/schemas/AppsClientApp"
                    }
                  },
                  "required": [
                    "client_app"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/AppsError"
          }
        }
      }
    },
    "/clients/{id_client}/apps/{id_app}": {
      "get": {
        "tags": [
          "apps"
        ],
        "summary": "Get one app",
        "description": "The app's record, without its secret. Note the envelope: `{client_app}` here, `{client_apps, total}` in the list.\n\n**An app token works here.** This is the one part of `/apps` that does: an app can read and update its own record, but it cannot list, create, delete, or read the secret. The **Custom plan** is still required (`ERROR_CODE_542`).",
        "operationId": "getClientApp",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AppsidClient"
          },
          {
            "$ref": "#/components/parameters/AppsidApp"
          }
        ],
        "responses": {
          "200": {
            "description": "The app",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "client_app": {
                      "$ref": "#/components/schemas/AppsClientApp"
                    }
                  },
                  "required": [
                    "client_app"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/AppsError"
          }
        }
      },
      "put": {
        "tags": [
          "apps"
        ],
        "summary": "Update an app",
        "description": "Replaces the app's configuration. **Every field is overwritten with what you send**, so a `redirect_urls` you leave out becomes an empty list — send the whole object, not a patch.\n\nChanging `webhook_url` takes effect on the next event; there is no verification handshake.\n\n**It replaces every field with what the body carries.** `name`, `keycloak_client_idenfifier`, `allowed_domains`, `redirect_urls` and `webhook_url` are all written as sent, so omitting one erases it — sending an update without `webhook_url` turns the webhook off. Read the app first and send it back whole.\n\n**An app token works here.** This is the one part of `/apps` that does: an app can read and update its own record, but it cannot list, create, delete, or read the secret. The **Custom plan** is still required (`ERROR_CODE_542`).",
        "operationId": "updateClientApp",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AppsidClient"
          },
          {
            "$ref": "#/components/parameters/AppsidApp"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AppsClientAppInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated app",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "client_app": {
                      "$ref": "#/components/schemas/AppsClientApp"
                    }
                  },
                  "required": [
                    "client_app"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/AppsError"
          }
        }
      },
      "delete": {
        "tags": [
          "apps"
        ],
        "summary": "Delete an app",
        "description": "Revokes the credentials and marks the app as deleted. **Tokens already issued stop working**: the token endpoint checks that the app still exists before answering, and requests carrying an old token no longer resolve to an app.\n\n**Needs a USER token, not an app token.** With client credentials it answers `ERROR_CODE_512`. It also needs the **Custom plan** (`ERROR_CODE_542`): apps are what that plan adds, and they are the door to this whole API.",
        "operationId": "deleteClientApp",
        "x-planvortex-identity": [
          "current_user"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AppsidClient"
          },
          {
            "$ref": "#/components/parameters/AppsidApp"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/AppsError"
          }
        }
      }
    },
    "/clients/{id_client}/apps/{id_app}/secret": {
      "get": {
        "tags": [
          "apps"
        ],
        "summary": "Read the app's secret",
        "description": "Returns the `client_secret` in clear. It is a separate endpoint precisely so that the secret never travels inside a listing that gets logged, cached or drawn on a screen by accident.\n\nThe same secret is what signs the outgoing webhooks (`x-hub-signature`, `x-hub-signature-256`).\n\n**Needs a USER token, not an app token.** With client credentials it answers `ERROR_CODE_512`. It also needs the **Custom plan** (`ERROR_CODE_542`): apps are what that plan adds, and they are the door to this whole API.\n\nIt comes straight from Keycloak, so it is the live secret and not a copy: treat the response as a credential and never log it.",
        "operationId": "getClientAppSecret",
        "x-planvortex-identity": [
          "current_user"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AppsidClient"
          },
          {
            "$ref": "#/components/parameters/AppsidApp"
          }
        ],
        "responses": {
          "200": {
            "description": "The secret",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "secret": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "secret"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/AppsError"
          }
        }
      }
    },
    "/social_networks": {
      "get": {
        "tags": [
          "catalog"
        ],
        "summary": "List the supported social networks",
        "description": "Every network PlanVortex can connect, as a flat array of identifiers.\n\nThis is the list every other enumeration in the API is built from. It grows several times a year, so a client that hardcodes it will reject a value it should have accepted.",
        "operationId": "getSocialNetworks",
        "x-planvortex-identity": [
          "current_user",
          "current_app",
          "temporal_token"
        ],
        "responses": {
          "200": {
            "description": "Supported networks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SocialNetwork"
                  }
                },
                "example": [
                  "facebook",
                  "twitter",
                  "instagram",
                  "linkedin",
                  "whatsapp",
                  "tiktok",
                  "youtube",
                  "google_business",
                  "bluesky",
                  "discord"
                ]
              }
            }
          },
          "400": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/allowed_social_publications": {
      "get": {
        "tags": [
          "catalog"
        ],
        "summary": "List the networks that can publish",
        "description": "The subset of `/social_networks` that accepts publications. `google_business` is not in it: a local listing receives reviews, it does not receive posts.",
        "operationId": "getAllowedSocialPublications",
        "x-planvortex-identity": [
          "current_user",
          "current_app",
          "temporal_token"
        ],
        "responses": {
          "200": {
            "description": "Networks that accept publications",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SocialNetwork"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/allowed_social_messages": {
      "post": {
        "tags": [
          "catalog"
        ],
        "summary": "List the networks with direct messages",
        "description": "The subset of `/social_networks` whose conversations can be read and answered from PlanVortex.\n\n**It is a POST and it takes no body.** That is historical, not a design decision, and it is kept as it is because changing the verb would break every integration already calling it. Send an empty body.",
        "operationId": "getAllowedSocialMessages",
        "x-planvortex-identity": [
          "current_user",
          "current_app",
          "temporal_token"
        ],
        "responses": {
          "200": {
            "description": "Networks with conversations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SocialNetwork"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/social_limits": {
      "get": {
        "tags": [
          "catalog"
        ],
        "summary": "Per-network publication limits",
        "description": "Every limit a publication is validated against, indexed by limit and then by network.\n\nRead it before building a composer. A few traps worth knowing:\n\n• **`characters` is not always the only text limit.** Bluesky counts 300 *graphemes* **and** 3.000 *bytes*; the second one travels in `max_post_bytes`, where `0` means \"this network does not measure text in bytes\". A family emoji is one grapheme and eleven UTF-16 units, so counting with `String.length` is wrong in both directions.\n• **`0` in `title_characters` means the network has no title field**, not a title of zero length.\n• **`comment_characters` is a different limit from `characters`.** Facebook takes 63.206 in a post and 8.000 in a comment.\n• Every network in `/social_networks` appears in every map. A missing key is a bug, and the backend's conformance suite fails on it.",
        "operationId": "getSocialLimits",
        "x-planvortex-identity": [
          "current_user",
          "current_app",
          "temporal_token"
        ],
        "responses": {
          "200": {
            "description": "Limits per network",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CatalogSocialLimits"
                }
              }
            }
          },
          "400": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/publication_limits": {
      "get": {
        "tags": [
          "catalog"
        ],
        "summary": "Publication limits that do not depend on the network",
        "description": "Limits that apply to a publication whatever network it goes to. Today there is one: how many times a failed publication can be retried by hand with `POST /organizations/{id_organization}/publish/{id_publication}/retry`.\n\nIt is published for the same reason as `/social_limits`: a 3 written into a client stays a 3 the day the server raises it to 5, and the button would promise a retry the API refuses.",
        "operationId": "getPublicationLimits",
        "x-planvortex-identity": [
          "current_user",
          "current_app",
          "temporal_token"
        ],
        "responses": {
          "200": {
            "description": "Publication limits",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "max_retries": {
                      "type": "integer",
                      "description": "How many manual retries a failed publication accepts before `retry` starts answering an error."
                    }
                  },
                  "required": [
                    "max_retries"
                  ]
                },
                "example": {
                  "max_retries": 3
                }
              }
            }
          },
          "400": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/allowed_aspect_ratios": {
      "get": {
        "tags": [
          "catalog"
        ],
        "summary": "Accepted aspect ratios per network",
        "description": "The crops each network accepts, as the numeric ratio used to validate and the label to show, side by side at the same index.\n\nThe keys are **not** always a network name: some networks have a different list per publication type, and those appear as `<network>_<type>` (`facebook_reels`, `facebook_stories`). A network missing from the map does not validate the ratio.",
        "operationId": "getAllowedAspectRatios",
        "x-planvortex-identity": [
          "current_user",
          "current_app",
          "temporal_token"
        ],
        "responses": {
          "200": {
            "description": "Aspect ratios per network",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "$ref": "#/components/schemas/CatalogAspectRatios"
                  }
                },
                "example": {
                  "instagram": {
                    "values": [
                      1,
                      0.8,
                      1.91,
                      0.5625
                    ],
                    "text": [
                      "1:1",
                      "4:5",
                      "1.91:1",
                      "9:16"
                    ]
                  },
                  "facebook_reels": {
                    "values": [
                      0.5625
                    ],
                    "text": [
                      "9:16"
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/clients": {
      "get": {
        "tags": [
          "clients"
        ],
        "summary": "Return all available clients",
        "description": "Return a list of clients ordered by creation date (desc)",
        "operationId": "getClients",
        "x-planvortex-identity": [
          "current_user",
          "current_app",
          "temporal_token"
        ],
        "parameters": [
          {
            "name": "getUse",
            "in": "query",
            "description": "Boolean determine if the server will response with the actual use of each client",
            "required": false,
            "explode": true,
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Number of records from which you will get clients (pagination)",
            "required": false,
            "explode": true,
            "schema": {
              "type": "integer",
              "default": "0"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "The limit of records will be retrieved (pagination)",
            "required": false,
            "explode": true,
            "schema": {
              "type": "integer",
              "default": "10"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientsClientList"
                }
              }
            }
          },
          "400": {
            "description": "Error. Classify by `code` in the body, never by the HTTP status: every domain error travels with HTTP 400.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/clients/{id_client}": {
      "get": {
        "tags": [
          "clients"
        ],
        "summary": "Return a client by identifier",
        "description": "Return a client by identifier",
        "operationId": "getClient",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_client",
            "in": "path",
            "description": "Client identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "getUse",
            "in": "query",
            "description": "Boolean determine if the server will response with the actual use of each client",
            "required": false,
            "explode": true,
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientsClientOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `521` | Invalid client. The identifier is invalid o doesn't exists |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      },
      "put": {
        "tags": [
          "clients"
        ],
        "summary": "Update a client by identifier",
        "description": "Update a client by identifier",
        "operationId": "updateClient",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_client",
            "in": "path",
            "description": "Client identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientsClientOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `521` | Invalid client. The identifier is invalid o doesn't exists |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Only `name` and `client_type` are read. Omitted properties keep their current value.",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Client name."
                  },
                  "client_type": {
                    "type": "string",
                    "enum": [
                      "personal",
                      "professional",
                      "enterprise",
                      "agency"
                    ],
                    "description": "Type of client."
                  }
                }
              },
              "examples": {
                "rename": {
                  "summary": "Rename the client",
                  "value": {
                    "name": "Acme Studio",
                    "client_type": "agency"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/clients/{id_client}/organizations": {
      "post": {
        "tags": [
          "clients"
        ],
        "summary": "Create a new root organization inside a client",
        "description": "Within a client, organizations can be created to separate and isolate data between them",
        "operationId": "addParentOrganization",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_client",
            "in": "path",
            "description": "Client the organization will belong to",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientsOrganizationOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1400` | Account limit reached for the organization plan |\n| `1401` | Publication limit reached for the organization plan |\n| `1402` | Users limit reached for the organization plan |\n| `1403` | Shared space limit reached for the organization plan |\n| `1407` | Twitter credits limit reached for the organization plan. The sum of twitter_credits assigned to the client's organizations cannot exceed the client's plan twitter_credits. |\n| `1408` | AI credits limit reached for the organization plan. The sum of ai_credits assigned to the client's organizations cannot exceed the client's plan ai_credits. |\n| `521` | Invalid client. The identifier is invalid o doesn't exists |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "requestBody": {
          "required": true,
          "description": "Creates a top-level organization for the client. `parent_organization` is ignored: this endpoint always creates a root organization.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClientsOrganizationCreate"
              },
              "examples": {
                "basic": {
                  "summary": "Create an organization with its plan",
                  "value": {
                    "name": "Client X",
                    "actual_plan": {
                      "accounts": 4,
                      "publications": 150,
                      "users": 1,
                      "space": 0.25,
                      "integrations": 1,
                      "twitter_credits": 2500,
                      "ai_credits": 1000
                    }
                  }
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "clients"
        ],
        "summary": "Get root organizations inside client",
        "description": "Return a list of organizations inside the client",
        "operationId": "getClientOrganizations",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_client",
            "in": "path",
            "description": "Client identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "getUse",
            "in": "query",
            "description": "Boolean determine if the server will response with the actual use of each client",
            "required": false,
            "explode": true,
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Number of records from which you will get clients (pagination)",
            "required": false,
            "explode": true,
            "schema": {
              "type": "integer",
              "default": "0"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "The limit of records will be retrieved (pagination)",
            "required": false,
            "explode": true,
            "schema": {
              "type": "integer",
              "default": "10"
            }
          },
          {
            "name": "name",
            "in": "query",
            "description": "The limit of records will be retrieved (pagination)",
            "required": false,
            "explode": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientsOrganizationList"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `521` | Invalid client. The identifier is invalid o doesn't exists |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/clients/{id_client}/organizations/{id_organization}": {
      "put": {
        "tags": [
          "clients"
        ],
        "summary": "Update a root organization by identifier",
        "description": "Update data from the received organization",
        "operationId": "updateParentOrganization",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_client",
            "in": "path",
            "description": "Client the organization belongs to",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientsOrganizationOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `1400` | Account limit reached for the organization plan |\n| `1401` | Publication limit reached for the organization plan |\n| `1402` | Users limit reached for the organization plan |\n| `1403` | Shared space limit reached for the organization plan |\n| `1407` | Twitter credits limit reached for the organization plan. The sum of twitter_credits assigned to the client's organizations cannot exceed the client's plan twitter_credits. |\n| `1408` | AI credits limit reached for the organization plan. The sum of ai_credits assigned to the client's organizations cannot exceed the client's plan ai_credits. |\n| `1301` | Client plan invalid. Min accounts must be X |\n| `1302` | Client plan invalid. Min publications must be X |\n| `1303` | Client plan invalid. Min users must be at least X |\n| `1306` | Client plan invalid. Min shared space must be at least X |\n| `521` | Invalid client. The identifier is invalid o doesn't exists |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClientsOrganizationUpdate"
              },
              "examples": {
                "changePlan": {
                  "summary": "Reassign resources",
                  "value": {
                    "actual_plan": {
                      "accounts": 8,
                      "publications": 300,
                      "users": 3,
                      "space": 1,
                      "integrations": 3,
                      "twitter_credits": 5000,
                      "ai_credits": 4000
                    }
                  }
                },
                "stopTwitterStats": {
                  "summary": "Stop spending X credits on automatic stats",
                  "value": {
                    "stats_settings": {
                      "auto_refresh_twitter": false
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "clients"
        ],
        "summary": "Delete a root organization by identifier",
        "description": "Delete the root organization and all its resources",
        "operationId": "deleteClientOrganization",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `521` | Invalid client. The identifier is invalid o doesn't exists |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_client",
            "in": "path",
            "description": "Client identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/clients/{id_client}/ai-settings": {
      "put": {
        "tags": [
          "clients"
        ],
        "summary": "Configure the client's own AI models (BYOK)",
        "description": "Set the client's Bring-Your-Own-Key AI provider configuration per scope. The request body is an object keyed by scope (orchestrator, text, image); a scope set to null clears it. Providers and capabilities are validated against a fixed catalogue. When a scope uses BYOK, its calls are billed to the client's own provider and do NOT consume PlanVortex AI credits, but the artificial_inteligence plan flag is still required to use the feature. API keys are stored encrypted and never returned; the response client only exposes provider, model and has_api_key per scope. Requires the client:update permission.",
        "operationId": "updateAiSettings",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "requestBody": {
          "description": "AI provider configuration keyed by scope",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClientsAiSettings"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful operation. Returns the client with the sanitized ai_settings (no API keys).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientsClientOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `2104` | Invalid AI provider, scope or model. Returned when the scope is unknown, the provider is not in the catalogue, the provider does not support the scope's capability, or model/api_key are missing. |\n| `521` | Invalid client. The identifier is invalid or doesn't exist |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "parameters": [
          {
            "name": "id_client",
            "in": "path",
            "description": "Client identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/clients_organizations": {
      "get": {
        "tags": [
          "clients"
        ],
        "summary": "Every client with its organizations, in one call",
        "description": "The whole tree the caller can see: their clients, and inside each one the organizations they have access to.\n\nIt exists to save the N+1 that building a client picker otherwise costs. **An app always gets exactly one client** — its own — with every organization of it.\n\nThe two pairs of pagination parameters are independent: `offset`/`limit` page the clients, `offsetOrganizations`/`limitOrganizations` page the organizations inside each one.",
        "operationId": "getClientsWithOrganizations",
        "x-planvortex-identity": [
          "current_user",
          "current_app",
          "temporal_token"
        ],
        "parameters": [
          {
            "name": "offset",
            "in": "query",
            "description": "Clients to skip",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Clients per page",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "offsetOrganizations",
            "in": "query",
            "description": "Organizations to skip inside each client",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "limitOrganizations",
            "in": "query",
            "description": "Organizations per page inside each client",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "getUse",
            "in": "query",
            "required": false,
            "description": "Send `true` to add each client's plan consumption. It costs several aggregations per client, so leave it off for a picker.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Clients, each with its organizations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "clients": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/ClientsClient"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "organizations": {
                                "type": "array",
                                "items": {
                                  "$ref": "#/components/schemas/Organization"
                                }
                              },
                              "total": {
                                "type": "integer",
                                "description": "Organizations this client has, ignoring the pagination."
                              }
                            },
                            "required": [
                              "organizations",
                              "total"
                            ]
                          }
                        ]
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Clients the caller can see."
                    }
                  },
                  "required": [
                    "clients",
                    "total"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `521` | Invalid client. The identifier is invalid or doesn't exist |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/clients/{id_client}/roles": {
      "post": {
        "tags": [
          "client_roles"
        ],
        "summary": "Create a new client role",
        "description": "Allow the creation of new client roles",
        "operationId": "createClientRole",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_client",
            "in": "path",
            "description": "Client the role belongs to",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientsRolesRoleOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `521` | Invalid client. The identifier is invalid o doesn't exists |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "requestBody": {
          "required": true,
          "description": "Creates a client-level role. Valid values for `permissions` are the client permissions listed in the schema.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClientsRolesRoleInput"
              },
              "examples": {
                "readOnly": {
                  "summary": "Read-only role",
                  "value": {
                    "name": "Viewer",
                    "permissions": [
                      "client:read",
                      "client_organization:read",
                      "client_organization_publications:read"
                    ]
                  }
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "client_roles"
        ],
        "summary": "Get a list of roles",
        "description": "Return a list of client roles inside the received client",
        "operationId": "getClientRoles",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_client",
            "in": "path",
            "description": "Client identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Number of records from which you will get roles (pagination)",
            "required": false,
            "explode": true,
            "schema": {
              "type": "integer",
              "default": "0"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "The limit of records will be retrieved (pagination)",
            "required": false,
            "explode": true,
            "schema": {
              "type": "integer",
              "default": "10"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientsRolesRoleList"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `521` | Invalid client. The identifier is invalid o doesn't exists |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/clients/{id_client}/roles/{id_rol}": {
      "delete": {
        "tags": [
          "client_roles"
        ],
        "summary": "Delete a client rol",
        "description": "Delete the client role received in parameters",
        "operationId": "deleteRole",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1203` | Invalid client role |\n| `521` | Invalid client. The identifier is invalid o doesn't exists |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "parameters": [
          {
            "name": "id_rol",
            "in": "path",
            "description": "Rol identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_client",
            "in": "path",
            "description": "Client identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "put": {
        "tags": [
          "client_roles"
        ],
        "summary": "Update a client rol",
        "description": "Update the client role received in parameters",
        "operationId": "updateClientRole",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_client",
            "in": "path",
            "description": "Client the role belongs to",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_rol",
            "in": "path",
            "description": "Role identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientsRolesRoleOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1203` | Invalid client role |\n| `521` | Invalid client. The identifier is invalid o doesn't exists |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClientsRolesRoleInput"
              },
              "examples": {
                "addPermission": {
                  "summary": "Replace name and permissions",
                  "value": {
                    "name": "Editor",
                    "permissions": [
                      "client:read",
                      "client_organization_publications:create",
                      "client_organization_publications:update"
                    ]
                  }
                }
              }
            }
          }
        }
      }
    },
    "/clients/{id_client}/roles/{id_rol}/users/{id_user}": {
      "post": {
        "tags": [
          "client_roles"
        ],
        "summary": "Adds the received client role to the received user",
        "description": "Adds the received client role to the received user. The user is assigned to he received role",
        "operationId": "addUserRole",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1203` | Invalid client role |\n| `605` | User id invalid |\n| `521` | Invalid client. The identifier is invalid o doesn't exists |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "parameters": [
          {
            "name": "id_user",
            "in": "path",
            "description": "User identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_rol",
            "in": "path",
            "description": "Rol identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_client",
            "in": "path",
            "description": "Client identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "delete": {
        "tags": [
          "client_roles"
        ],
        "summary": "delete the received user from the received client role",
        "description": "delete the received user from the received client role",
        "operationId": "removeUserRole",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1203` | Invalid client role |\n| `605` | User id invalid |\n| `521` | Invalid client. The identifier is invalid o doesn't exists |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "parameters": [
          {
            "name": "id_user",
            "in": "path",
            "description": "User identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_rol",
            "in": "path",
            "description": "Rol identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_client",
            "in": "path",
            "description": "Client identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/clients/{id_client}/roles/{id_rol}/users": {
      "get": {
        "tags": [
          "client_roles"
        ],
        "summary": "Get all users of the client role",
        "description": "Retrieve all users of the received client role",
        "operationId": "getUsersInClientRole",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_rol",
            "in": "path",
            "description": "Rol identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_client",
            "in": "path",
            "description": "Client identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Number of records from which you will get users (pagination)",
            "required": false,
            "explode": true,
            "schema": {
              "type": "integer",
              "default": "0"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "The limit of records will be retrieved (pagination)",
            "required": false,
            "explode": true,
            "schema": {
              "type": "integer",
              "default": "10"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientsRolesUserList"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1203` | Invalid client role |\n| `521` | Invalid client. The identifier is invalid o doesn't exists |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/clients/{id_client}/users": {
      "get": {
        "tags": [
          "client_roles"
        ],
        "summary": "Get all users of the client in any role",
        "description": "Retrieve all users of the received client in any role",
        "operationId": "getClientUsers",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_client",
            "in": "path",
            "description": "Client identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Number of records from which you will get users (pagination)",
            "required": false,
            "explode": true,
            "schema": {
              "type": "integer",
              "default": "0"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "The limit of records will be retrieved (pagination)",
            "required": false,
            "explode": true,
            "schema": {
              "type": "integer",
              "default": "10"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientsRolesUserList"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `521` | Invalid client. The identifier is invalid o doesn't exists |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/clients/{id_client}/roles/{id_rol}/invite": {
      "post": {
        "tags": [
          "client_roles"
        ],
        "summary": "Invite an user to a client role",
        "description": "Invite an user to a client role by the email. The user will recieve an invitation to the email",
        "operationId": "inviteRole",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_client",
            "in": "path",
            "description": "Client the role belongs to",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_rol",
            "in": "path",
            "description": "Role the invited user will be assigned",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1203` | Invalid client role |\n| `609` | Invalid email |\n| `521` | Invalid client. The identifier is invalid o doesn't exists |\n| `508` | Error sending the invitation |\n| `509` | User already invited to the rol |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "Email address to invite. Returns error 609 if it is not a valid address. If no user exists with that address, a disabled one is created and an invitation email is sent with a temporary token; the account is enabled when the invitation is accepted."
                  }
                }
              },
              "examples": {
                "invite": {
                  "summary": "Invite a user to this role",
                  "value": {
                    "email": "someone@example.com"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/clients_permissions": {
      "get": {
        "tags": [
          "client_roles"
        ],
        "summary": "Retrieve the list of client permissions",
        "description": "Retrieve all client permissions that are allowed in client role creation",
        "operationId": "getClientPermissions",
        "x-planvortex-identity": [
          "current_user",
          "current_app",
          "temporal_token"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientsRolesClientPermissionsOne"
                }
              }
            }
          },
          "400": {
            "description": "Error. Classify by `code` in the body, never by the HTTP status: every domain error travels with HTTP 400.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/organizations/{id_organization}/comments": {
      "get": {
        "tags": [
          "comments"
        ],
        "summary": "The inbox: first-level comments across the whole organization",
        "description": "Served from PlanVortex's database, so it costs nothing and calls no social network. It is a **snapshot**: `collected_date` says when each row was last read. Open a thread to see what the network says right now.\n\nOrdered by `creation_date` descending — the date on the network, not the date we collected it — so rows from six networks interleave correctly.\n\n**Your own replies are not in here.** Anything with `author.is_own: true` is filtered out: what you wrote is not incoming mail. They are still stored, and they do show up in the thread.\n\nRequires the `comments:read` permission (`client_organization_comments:read` for apps) and a paid plan.",
        "operationId": "getComments",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/CommentsidOrganization"
          },
          {
            "name": "social_network",
            "in": "query",
            "required": false,
            "description": "Filter by network. **Repeat the parameter** for several (`?social_network=youtube&social_network=instagram`); a comma-separated string is read as one network name and silently matches nothing.",
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/CommentsCommentNetworkName"
              }
            }
          },
          {
            "name": "id_account",
            "in": "query",
            "required": false,
            "description": "Only comments on this connected account",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_publication",
            "in": "query",
            "required": false,
            "description": "Only comments on this publication of yours. Reviews never match it: a review hangs off the listing and has no publication.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "unread",
            "in": "query",
            "required": false,
            "description": "Only unread ones. Any value other than the literal `false` turns the filter on.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Case-insensitive substring match on the comment text",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "rating",
            "in": "query",
            "required": false,
            "description": "Only comments with these star ratings. **Repeat the parameter** for several (`?rating=1&rating=2`), which is the usual way to work a listing: show me the one- and two-star reviews first.\n\nOnly review networks carry a rating, so this filter leaves everything else out — it is not a way to select \"comments without a rating\".",
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "integer",
                "minimum": 1,
                "maximum": 5
              }
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "comments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CommentsComment"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Rows matching the filters, not rows returned"
                    }
                  },
                  "required": [
                    "comments",
                    "total"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CommentsCommentError"
          }
        }
      }
    },
    "/organizations/{id_organization}/unread_comments": {
      "get": {
        "tags": [
          "comments"
        ],
        "summary": "How many comments are waiting",
        "description": "The badge number. Counts unread, non-deleted comments of the organization, leaving out the ones you wrote yourself.\n\nRequires the `comments:read` permission (`client_organization_comments:read` for apps) and a paid plan.",
        "operationId": "getTotalUnreadComments",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/CommentsidOrganization"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "total"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CommentsCommentError"
          }
        }
      }
    },
    "/organizations/{id_organization}/publish/{id_publication}/comments": {
      "get": {
        "tags": [
          "comments"
        ],
        "summary": "The thread of a publication, read live",
        "description": "Asks the social network and reconciles with what is stored: the network wins on text, counters and existence; the stored copy only contributes `_id`, `read` and `replied`. Comments the network no longer returns are marked deleted and stop appearing in the inbox.\n\n**On X this call costs money.** X bills per unit read, so the response carries `credits_consumed` with what this particular read spent from the client's monthly pool. It is `0` on every other network. Charging happens after the read and by real units: a failed call charges nothing, and a page with three replies is not charged for fifty.\n\nUse this one when the comment has an `id_publication`. When it does not — a review hangs off a listing — use the per-account endpoint instead.\n\nRequires the `comments:read` permission (`client_organization_comments:read` for apps) and a paid plan.",
        "operationId": "getPublicationComments",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/CommentsidOrganization"
          },
          {
            "$ref": "#/components/parameters/CommentsidPublication"
          },
          {
            "$ref": "#/components/parameters/CommentsthreadLimit"
          },
          {
            "$ref": "#/components/parameters/CommentsthreadOffset"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommentsCommentThread"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CommentsCommentError"
          }
        }
      }
    },
    "/organizations/{id_organization}/accounts/{id_account}/comments": {
      "get": {
        "tags": [
          "comments"
        ],
        "summary": "The thread of an account, read live",
        "description": "The twin of the publication endpoint, for networks whose comments **do not hang off a publication of yours**.\n\nToday that is Google Business: a review hangs off the **listing**, so the account has no publication to ask about. Its `publication_external_id` is the listing itself (`locations/{id}`) and `id_publication` is always absent.\n\nSame reconciliation, same permissions, same shape of response as the publication thread. Pick between the two by looking at the comment: if it has an `id_publication`, use that one; if it does not, use this.\n\nOne behaviour differs underneath, and it matters if you are reasoning about deletions: **reviews are exempt from the deletion sweep**. Google's review listing cannot be ordered by creation date (only by last update or by rating), so the rule that says \"newer than the oldest row on this page and missing from it means it is gone\" does not hold, and applying it would empty most of a busy listing's inbox in one pass. The trade is deliberate: a review Google removes may linger, which is rare, rather than good rows disappearing, which is not recoverable.\n\nRequires the `comments:read` permission (`client_organization_comments:read` for apps) and a paid plan.",
        "operationId": "getAccountComments",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/CommentsidOrganization"
          },
          {
            "$ref": "#/components/parameters/CommentsidAccount"
          },
          {
            "$ref": "#/components/parameters/CommentsthreadLimit"
          },
          {
            "$ref": "#/components/parameters/CommentsthreadOffset"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommentsCommentThread"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CommentsCommentError"
          }
        }
      }
    },
    "/organizations/{id_organization}/comments/{id_comment}/replies": {
      "get": {
        "tags": [
          "comments"
        ],
        "summary": "The replies to a comment, read live",
        "description": "Second level, read against the network like the thread — and, on X, charged the same way.\n\nYour own replies written in the last few minutes are injected into the first page even when the network does not list them yet: networks index what you just wrote with a delay, and without this you would post a reply, refresh, and see nothing.\n\nOn Google Business a review has **at most one** reply — yours — so this returns zero or one row and there is no tree to walk.\n\nRequires the `comments:read` permission (`client_organization_comments:read` for apps) and a paid plan.",
        "operationId": "getCommentReplies",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/CommentsidOrganization"
          },
          {
            "$ref": "#/components/parameters/CommentsidComment"
          },
          {
            "$ref": "#/components/parameters/CommentsthreadLimit"
          },
          {
            "$ref": "#/components/parameters/CommentsthreadOffset"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommentsCommentThread"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CommentsCommentError"
          }
        }
      }
    },
    "/organizations/{id_organization}/comments/{id_comment}/reply": {
      "post": {
        "tags": [
          "comments"
        ],
        "summary": "Reply in public",
        "description": "Posts a **public** reply on the social network. This is not the same as replying privately to whoever commented — that is a Messenger feature and lives in the messaging endpoints.\n\nThe reply is stored immediately, with `author.is_own: true`, without waiting for the network to index it, and the comment it answers is left `replied: true` and `read: true` with `our_reply_external_id` pointing at it.\n\n**On Google Business this is an upsert.** A review has at most one reply, so replying again does not add a second one: it replaces the text of the one that is there, and the stored row is updated in place. Label your button accordingly — \"reply\" the first time, \"edit reply\" afterwards.\n\nThe text is validated against that network's limit before anything is sent (`comment_characters` in `GET /social_limits`); an empty or over-long text returns error `948` with the limit in `data.max`.\n\n**On X this costs credits**: 15, or 200 if the text contains a link, charged only on success.\n\nRequires the `comments:create` permission (`client_organization_comments:create` for apps) and a paid plan.",
        "operationId": "replyComment",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/CommentsidOrganization"
          },
          {
            "$ref": "#/components/parameters/CommentsidComment"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "text"
                ],
                "properties": {
                  "text": {
                    "type": "string",
                    "description": "The public reply. Must not be empty and must fit the network's `comment_characters` limit."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reply published",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "comment": {
                      "$ref": "#/components/schemas/CommentsComment"
                    },
                    "reply": {
                      "description": "Your reply as it was stored. **It can arrive without `_id`**: the reply is published on the network first and stored afterwards, and storing it is deliberately not allowed to fail the request — the network already published it. When that write fails you get what the network returned, which has no PlanVortex identifier. `comment.our_reply_external_id` is the field that always identifies it.",
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/CommentsComment"
                        }
                      ]
                    },
                    "credits_consumed": {
                      "type": "integer",
                      "description": "X credits this reply spent. `0` on every other network."
                    }
                  },
                  "required": [
                    "comment",
                    "reply",
                    "credits_consumed"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CommentsCommentError"
          }
        }
      }
    },
    "/organizations/{id_organization}/comments/{id_comment}": {
      "put": {
        "tags": [
          "comments"
        ],
        "summary": "Mark as read, or hide on the network",
        "description": "Two different kinds of change, accepted together because a panel changes them from the same place:\n\n- `read` is **yours**. It touches no social network, costs nothing and can always be set.\n- `hidden` **touches the network**, and not every network allows it — LinkedIn and Google Business have no hide endpoint at all and return error `946`. Check `GET /social_comment_actions` before offering the control.\n\nRequires the `comments:create` permission (`client_organization_comments:create` for apps) and a paid plan.",
        "operationId": "updateComment",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/CommentsidOrganization"
          },
          {
            "$ref": "#/components/parameters/CommentsidComment"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "read": {
                    "type": "boolean",
                    "description": "Your own state. Only the literal `true` marks it read."
                  },
                  "hidden": {
                    "type": "boolean",
                    "description": "Hide or show it **on the social network**. Only the literal `true` hides."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "comment": {
                      "$ref": "#/components/schemas/CommentsComment"
                    }
                  },
                  "required": [
                    "comment"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CommentsCommentError"
          }
        }
      },
      "delete": {
        "tags": [
          "comments"
        ],
        "summary": "Delete the comment on the social network",
        "description": "Deletes it **on the network** and marks the row `deleted: true` here. The row is kept on purpose rather than removed: if it were removed, the next read of the thread — or the webhook, which repeats — would create it again.\n\nWhich comments you may delete depends on the network and on whose comment it is, and the two cases use different permissions of the network's own: `delete_own` for yours, `delete_others` for somebody else's. Instagram and X refuse the second; **on Google Business the only thing that can be deleted is your own reply**, never a review. Reading `GET /social_comment_actions` first is the difference between a button that works and one that always errors.\n\nOn X this costs credits.\n\nRequires the `comments:delete` permission (`client_organization_comments:delete` for apps) and a paid plan.",
        "operationId": "deleteComment",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/CommentsidOrganization"
          },
          {
            "$ref": "#/components/parameters/CommentsidComment"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted on the network",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "success"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CommentsCommentError"
          }
        }
      }
    },
    "/social_capabilities": {
      "get": {
        "tags": [
          "catalog"
        ],
        "summary": "What every network supports",
        "description": "The full matrix network → capabilities. It is the **single source of truth** about what a network can do: the catalogue publishes it, the backend checks it before acting, and your integration should filter its account pickers with it rather than keeping a list of its own.\n\n`comments` is the coarse gate — whether the network has comments at all. Which *actions* it allows on one is a finer question and lives in `GET /social_comment_actions`, because the shape here is `{[capability]: boolean}` and nesting an object inside would break it.\n\nToday six networks answer `comments: true`: Facebook, Instagram, LinkedIn, X, YouTube and Google Business. TikTok and WhatsApp answer `false`, for reasons of theirs and not ours.\n\nOnly needs authentication.",
        "operationId": "getSocialCapabilities",
        "x-planvortex-identity": [
          "current_user",
          "current_app",
          "temporal_token"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "$ref": "#/components/schemas/CommentsSocialCapabilities"
                  }
                },
                "example": {
                  "instagram": {
                    "publications": true,
                    "messages": true,
                    "products": false,
                    "webhooks": true,
                    "persistent_menu": false,
                    "comments": true
                  },
                  "google_business": {
                    "publications": false,
                    "messages": false,
                    "products": false,
                    "webhooks": false,
                    "persistent_menu": false,
                    "comments": true
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error. Classify by `code` in the body, never by the HTTP status: every domain error travels with HTTP 400.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/social_comment_actions": {
      "get": {
        "tags": [
          "catalog"
        ],
        "summary": "What each network lets you do to a comment",
        "description": "The fine-grained matrix: reply, hide, delete your own, delete somebody else's — network by network. Read it before painting controls. Networks without comments answer `false` to all four.\n\nThe differences are real and each has a reason in the network's own API:\n\n| Network | reply | hide | delete_own | delete_others |\n| --- | --- | --- | --- | --- |\n| `facebook` | yes | yes | yes | yes |\n| `instagram` | yes | yes | yes | **no** — Instagram only lets you hide someone else's |\n| `youtube` | yes | yes | yes | yes — the channel owner really does moderate |\n| `linkedin` | yes | **no** — there is no hide endpoint | yes | yes |\n| `twitter` | yes | yes | yes | **no** — you cannot delete another account's post |\n| `google_business` | yes | **no** | yes — **your reply**, never the review | **no** |\n| `bluesky` | yes | yes — through the post's `threadgate` | yes | **no** — the reply lives in somebody else's repository |\n| `discord` | yes | **no** — Discord has no hide, only delete | yes | yes |\n| `tiktok`, `whatsapp` | no | no | no | no |\n\nOnly needs authentication.",
        "operationId": "getSocialCommentActions",
        "x-planvortex-identity": [
          "current_user",
          "current_app",
          "temporal_token"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "$ref": "#/components/schemas/CommentsCommentActions"
                  }
                },
                "example": {
                  "linkedin": {
                    "reply": true,
                    "hide": false,
                    "delete_own": true,
                    "delete_others": true
                  },
                  "google_business": {
                    "reply": true,
                    "hide": false,
                    "delete_own": true,
                    "delete_others": false
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error. Classify by `code` in the body, never by the HTTP status: every domain error travels with HTTP 400.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/organizations/{id_organization}/contacts": {
      "get": {
        "tags": [
          "contacts"
        ],
        "summary": "List the contacts of an organization",
        "description": "Newest first. `search` is a full-text search over the name, and `extra_data` filters by your own fields — which is what makes it possible to ask for \"the contacts of this campaign\" without keeping a second database.",
        "operationId": "getContacts",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "x-planvortex-plan": "paid",
        "parameters": [
          {
            "$ref": "#/components/parameters/ContactsidOrganization"
          },
          {
            "$ref": "#/components/parameters/Contactsoffset"
          },
          {
            "$ref": "#/components/parameters/Contactslimit"
          },
          {
            "name": "search",
            "in": "query",
            "description": "Full-text search over the contact's name.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "social_network",
            "in": "query",
            "description": "Only the contacts reachable on this network. It matches on `social_identifiers[].social_network`, so a contact with several channels comes back through any of them. It used to return an empty list always, because the server compared the whole array of objects against the network name; that was fixed on 2026-08-24.",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/SocialNetwork"
            }
          },
          {
            "name": "extra_data",
            "in": "query",
            "description": "Filter by your own fields, as `key`/`value` pairs. A `value` that is an array matches any of its entries.",
            "required": false,
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "key": {
                    "type": "string"
                  },
                  "value": {
                    "description": "A string, a number, or an array of either."
                  }
                }
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Contacts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "contacts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Contact"
                      }
                    },
                    "total": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "contacts",
                    "total"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ContactsError"
          }
        }
      },
      "post": {
        "tags": [
          "contacts"
        ],
        "summary": "Create a contact",
        "description": "Creates a contact by hand, which is how you bring people in from your own system.\n\n**Creating a contact does not let you write to them.** To send the first message the network has to allow it: WhatsApp needs an approved template, and Meta only allows an answer inside 24 hours of the contact writing first. What `social_identifiers` gives you is the link, not the permission.",
        "operationId": "createContact",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "x-planvortex-plan": "paid",
        "parameters": [
          {
            "$ref": "#/components/parameters/ContactsidOrganization"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactsContactCreate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created contact",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "contact": {
                      "$ref": "#/components/schemas/Contact"
                    }
                  },
                  "required": [
                    "contact"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ContactsError"
          }
        }
      },
      "delete": {
        "tags": [
          "contacts"
        ],
        "summary": "Delete every contact of an organization",
        "description": "Deletes all the contacts of the organization **and every message exchanged with them**. There is no confirmation step and no undo.\n\nNothing is deleted on the social networks: the conversations are still there, and reading an account again re-imports the contacts.",
        "operationId": "deleteOrganizationContacts",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "x-planvortex-plan": "paid",
        "parameters": [
          {
            "$ref": "#/components/parameters/ContactsidOrganization"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ContactsError"
          }
        }
      }
    },
    "/organizations/{id_organization}/contacts/{id_contact}": {
      "get": {
        "tags": [
          "contacts"
        ],
        "summary": "Get one contact",
        "description": "The contact's record. Note the envelope: this one answers `{contact}`, while the list answers `{contacts, total}`.",
        "operationId": "getContact",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "x-planvortex-plan": "paid",
        "parameters": [
          {
            "$ref": "#/components/parameters/ContactsidOrganization"
          },
          {
            "$ref": "#/components/parameters/ContactsidContact"
          }
        ],
        "responses": {
          "200": {
            "description": "The contact",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "contact": {
                      "$ref": "#/components/schemas/Contact"
                    }
                  },
                  "required": [
                    "contact"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ContactsError"
          }
        }
      },
      "put": {
        "tags": [
          "contacts"
        ],
        "summary": "Update a contact",
        "description": "Updates the contact's fields. **It answers `{success: true}`, not the updated contact** — read it back if you need the new state.",
        "operationId": "updateContact",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "x-planvortex-plan": "paid",
        "parameters": [
          {
            "$ref": "#/components/parameters/ContactsidOrganization"
          },
          {
            "$ref": "#/components/parameters/ContactsidContact"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactsContactUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated. **It does not return the contact**: read it again if you need the new state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ContactsError"
          }
        }
      },
      "delete": {
        "tags": [
          "contacts"
        ],
        "summary": "Delete a contact",
        "description": "Deletes the contact **and every message exchanged with them**. Nothing is deleted on the social network.",
        "operationId": "deleteContact",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "x-planvortex-plan": "paid",
        "parameters": [
          {
            "$ref": "#/components/parameters/ContactsidOrganization"
          },
          {
            "$ref": "#/components/parameters/ContactsidContact"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ContactsError"
          }
        }
      }
    },
    "/organizations/{id_organization}/dashboard": {
      "get": {
        "tags": [
          "dashboard"
        ],
        "summary": "The whole home screen in one call",
        "description": "Composes every block of the home screen in a single round trip: operational health, publications, publication metrics, account metrics, plan use, AI plans and unread messages.\n\n**A block the caller cannot read is omitted, not refused.** The endpoint only demands the minimum permission — reading the organization — and then checks each block on its own: somebody who can only see publications still gets their home instead of a 403. `available_blocks` says which blocks were allowed, so an absent block for lack of permission and an absent block for lack of data can be drawn differently.\n\nThe messages block is also omitted when the client's plan has no messaging.",
        "operationId": "getDashboard",
        "x-planvortex-identity": [
          "current_user",
          "current_app",
          "temporal_token"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/DashboardidOrganization"
          },
          {
            "$ref": "#/components/parameters/DashboardfromDate"
          },
          {
            "$ref": "#/components/parameters/DashboardtoDate"
          }
        ],
        "responses": {
          "200": {
            "description": "The dashboard",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DashboardDashboard"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DashboardError"
          }
        }
      }
    },
    "/organizations/{id_organization}/metrics": {
      "get": {
        "tags": [
          "dashboard"
        ],
        "summary": "Account metrics of the organization",
        "description": "Account statistics added up across the organization, on the axis asked for in `group_by`.\n\n**Not every metric adds up the same way.** `impressions` over a week is the sum of the seven days, but `followers` is the *last* value of each account: adding up seven snapshots of \"1.200 followers\" would report 8.400. The aggregation already knows which is which — do not add up the series yourself.\n\nA metric that a network does not publish is simply absent. It is not a zero.",
        "operationId": "getOrganizationMetrics",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/DashboardidOrganization"
          },
          {
            "$ref": "#/components/parameters/DashboardfromDate"
          },
          {
            "$ref": "#/components/parameters/DashboardtoDate"
          },
          {
            "name": "group_by",
            "in": "query",
            "description": "The axis of the answer. `day` for a series, `network` or `account` for a breakdown, `total` for one number per metric.",
            "required": false,
            "schema": {
              "type": "string",
              "default": "day",
              "enum": [
                "day",
                "network",
                "account",
                "total"
              ]
            }
          },
          {
            "name": "names",
            "in": "query",
            "description": "Only these metrics. Repeat the parameter for several. An unknown name answers error 1000 with the list of valid ones.",
            "required": false,
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/DashboardMetricName"
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Metrics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "range": {
                      "$ref": "#/components/schemas/DashboardDashboardRange"
                    },
                    "group_by": {
                      "type": "string",
                      "enum": [
                        "day",
                        "network",
                        "account",
                        "total"
                      ],
                      "description": "The axis that was applied. Defaults to `day`; anything else is rejected with 1000."
                    },
                    "stats": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DashboardMetricRow"
                      }
                    }
                  },
                  "required": [
                    "range",
                    "group_by",
                    "stats"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DashboardError"
          }
        }
      }
    },
    "/organizations/{id_organization}/publications/summary": {
      "get": {
        "tags": [
          "dashboard"
        ],
        "summary": "Publication counts of the range",
        "description": "How many publications there are by state, by network and by day.\n\n**`by_day` and `published_by_day` answer different questions over different sets.** `by_day` counts by *creation* date — work done — and `published_by_day` counts by *publish* date and only what actually went out. A publication written last month and published yesterday appears in the second and not in the first, which is the normal case for anybody scheduling two weeks ahead.",
        "operationId": "getPublicationsSummary",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/DashboardidOrganization"
          },
          {
            "$ref": "#/components/parameters/DashboardfromDate"
          },
          {
            "$ref": "#/components/parameters/DashboardtoDate"
          }
        ],
        "responses": {
          "200": {
            "description": "Counts",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "type": "object",
                      "properties": {
                        "range": {
                          "$ref": "#/components/schemas/DashboardDashboardRange"
                        }
                      },
                      "required": [
                        "range"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/DashboardPublicationsSummary"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DashboardError"
          }
        }
      }
    },
    "/organizations/{id_organization}/publications/top": {
      "get": {
        "tags": [
          "dashboard"
        ],
        "summary": "Best publications of the range",
        "description": "The publications with the highest value of one metric.\n\n**A publication whose network does not report that metric does not compete**, it is left out of the ranking rather than ranked as a zero. That is why asking for `saves` can return fewer rows than asking for `engagement`.",
        "operationId": "getTopPublications",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/DashboardidOrganization"
          },
          {
            "$ref": "#/components/parameters/DashboardfromDate"
          },
          {
            "$ref": "#/components/parameters/DashboardtoDate"
          },
          {
            "name": "metric",
            "in": "query",
            "description": "Metric to rank by.",
            "required": false,
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/DashboardMetricName"
                }
              ],
              "default": "engagement"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "How many publications to return.",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 5
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Top publications",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "range": {
                      "$ref": "#/components/schemas/DashboardDashboardRange"
                    },
                    "metric": {
                      "$ref": "#/components/schemas/DashboardMetricName"
                    },
                    "publications": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DashboardTopPublication"
                      }
                    }
                  },
                  "required": [
                    "range",
                    "metric",
                    "publications"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DashboardError"
          }
        }
      }
    },
    "/organizations/{id_organization}/use": {
      "get": {
        "tags": [
          "dashboard"
        ],
        "summary": "Plan consumption of the organization",
        "description": "What this organization has been assigned, what it is using, and the limits in force, in one call.\n\nIt exists so that drawing a progress bar does not mean asking for the client's whole organization list — which is what it used to take, and which needs a client-level permission an organization user has no reason to have.\n\nAn organization with no plan of its own inherits the closest parent that has one.",
        "operationId": "getOrganizationUse",
        "x-planvortex-identity": [
          "current_user",
          "current_app",
          "temporal_token"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/DashboardidOrganization"
          }
        ],
        "responses": {
          "200": {
            "description": "Consumption",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DashboardPlanUse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/DashboardError"
          }
        }
      }
    },
    "/integration_providers": {
      "get": {
        "tags": [
          "integrations"
        ],
        "summary": "Catalogue of available integration providers",
        "description": "What every provider is and what it can do: whether it connects through OAuth or a form, whether it contributes files to the library, whether it is polled for content, which file formats it accepts and which fields its configuration form takes.\n\nConsume this instead of hardcoding a provider list: a new provider shows up here without any client change.",
        "operationId": "getIntegrationProviders",
        "x-planvortex-identity": [
          "current_user",
          "current_app",
          "temporal_token"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "providers": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/IntegrationsIntegrationProvider"
                      }
                    }
                  },
                  "required": [
                    "providers"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Error. Classify by `code` in the body, never by the HTTP status: every domain error travels with HTTP 400.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/organizations/{id_organization}/integrations": {
      "get": {
        "tags": [
          "integrations"
        ],
        "summary": "List the integrations of an organization",
        "description": "Requires the `integrations:read` permission (`client_organization_integrations:read` for apps).",
        "operationId": "getIntegrations",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IntegrationsidOrganization"
          },
          {
            "name": "provider",
            "in": "query",
            "required": false,
            "description": "Filter by provider",
            "schema": {
              "$ref": "#/components/schemas/IntegrationsIntegrationProviderName"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "How many to return. **Without it there is no limit at all** and the whole list comes back — unlike every other listing in this API, which caps at 10."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "integrations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/IntegrationsIntegration"
                      }
                    },
                    "total": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "integrations",
                    "total"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Error. Classify by `code` in the body, never by the HTTP status: every domain error travels with HTTP 400.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "integrations"
        ],
        "summary": "Connect an integration",
        "description": "Creates the connection. The body depends on the provider:\n\n- `google_drive`: `{ provider, code }`, where `code` is the OAuth code returned to the redirect of `connect_link`. It is single-use, so the plan allowance is checked **before** the exchange.\n- `rss`: `{ provider, url, id_accounts, template?, publication_type?, auto_publish?, import_image? }`. The feed is read once to validate it and to take its title, and every item it already has is recorded as seen — connecting a blog never publishes its back catalogue.\n\nRequires the `integrations:create` permission.",
        "operationId": "addIntegration",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IntegrationsidOrganization"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/IntegrationsGoogleDriveConnectRequest"
                  },
                  {
                    "$ref": "#/components/schemas/IntegrationsRssConnectRequest"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Integration connected",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "integration": {
                      "$ref": "#/components/schemas/IntegrationsIntegration"
                    }
                  },
                  "required": [
                    "integration"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1404` | Integrations limit reached for the organization plan. On the free plan the allowance is 0, so this always fires. |\n| `2201` | Integration provider not supported (or the payload is missing what that provider needs). |\n| `2202` | Integration already connected for this organization and provider. Applies to OAuth providers; the body carries its `id_integration` in `data`, and the `code` you sent is still unused — post it to that integration's `/reconnect` to renew it. |\n| `2203` | Integration token expired or revoked, reconnect required. |\n| `2205` | Feed URL unreachable or not a valid feed. |\n| `2206` | Feed has no target accounts configured. |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/organizations/{id_organization}/integrations/{id_integration}": {
      "get": {
        "tags": [
          "integrations"
        ],
        "summary": "Get one integration",
        "description": "Requires the `integrations:read` permission.",
        "operationId": "getIntegration",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IntegrationsidOrganization"
          },
          {
            "$ref": "#/components/parameters/IntegrationsidIntegration"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "integration": {
                      "$ref": "#/components/schemas/IntegrationsIntegration"
                    }
                  },
                  "required": [
                    "integration"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`2200` Integration not found, or `1102` if it belongs to another organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "integrations"
        ],
        "summary": "Update an integration",
        "description": "Only what the user owns: its name, whether it is enabled and its `config` (merged, not replaced — `seen_guids` and `last_checked` belong to the job). Credentials are never updated here; for that, `POST .../reconnect`. Changing a feed's `url` here keeps the items it had already seen, so the new feed's back catalogue would be published on the next sweep — change a feed's URL through `/reconnect`, which revalidates it and marks its current items as seen.\n\nA disabled integration stops consuming plan allowance. Enabling one or changing its config clears `error_code`, so the job retries it.\n\nRequires the `integrations:update` permission.",
        "operationId": "updateIntegration",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IntegrationsidOrganization"
          },
          {
            "$ref": "#/components/parameters/IntegrationsidIntegration"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "enabled": {
                    "type": "boolean"
                  },
                  "config": {
                    "$ref": "#/components/schemas/IntegrationsRssConfig"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Integration updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "integration": {
                      "$ref": "#/components/schemas/IntegrationsIntegration"
                    }
                  },
                  "required": [
                    "integration"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Error. Classify by `code` in the body, never by the HTTP status: every domain error travels with HTTP 400.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "integrations"
        ],
        "summary": "Disconnect an integration",
        "description": "Revokes the credentials at the provider and deletes the connection.\n\nFiles already imported are **not** touched: they are your files, in your library, counting against your storage. Disconnecting Drive never empties anyone's library.\n\nRequires the `integrations:delete` permission.",
        "operationId": "deleteIntegration",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IntegrationsidOrganization"
          },
          {
            "$ref": "#/components/parameters/IntegrationsidIntegration"
          }
        ],
        "responses": {
          "200": {
            "description": "Integration deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "success"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Error. Classify by `code` in the body, never by the HTTP status: every domain error travels with HTTP 400.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/organizations/{id_organization}/integrations/{provider}/connect_link": {
      "get": {
        "tags": [
          "integrations"
        ],
        "summary": "Authorization URL of an OAuth provider",
        "description": "Only for providers whose `requires_oauth` is true; anything else returns `2201`. Send the user to the returned URL; the provider will come back to `{FRONT_APP}/connect/integration/{provider}` with a `code` to post to `POST /organizations/{id_organization}/integrations`.\n\nRequires the `integrations:create` **or** `integrations:update` permission: the consent link is needed both to connect a new integration and to reconnect one that already exists (`POST /organizations/{id_organization}/integrations/{id_integration}/reconnect`).",
        "operationId": "getIntegrationConnectLink",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IntegrationsidOrganization"
          },
          {
            "name": "provider",
            "in": "path",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/IntegrationsIntegrationProviderName"
            }
          },
          {
            "name": "redirect_uri",
            "in": "query",
            "required": false,
            "description": "Alternative return URL. It must be one of the redirects allowed for the installation, otherwise the request fails with `532`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "example": "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&..."
                    }
                  },
                  "required": [
                    "url"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Error. Classify by `code` in the body, never by the HTTP status: every domain error travels with HTTP 400.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/organizations/{id_organization}/integrations/{id_integration}/reconnect": {
      "post": {
        "tags": [
          "integrations"
        ],
        "summary": "Reconnect an integration",
        "description": "Renews the credentials of an integration that already exists — the OAuth token expired or the provider revoked it (`2203`) — or revalidates a feed's configuration, **on the same document**.\n\nThe body is the same one you would send to connect: `{ provider, code }` for an OAuth provider, or the form fields for a feed. It is handled by the same provider logic, so a feed that still cannot be read is still a `2205`.\n\nUse this instead of deleting and connecting again. Reconnecting keeps the `_id` (publications created by a feed reference it), the connection date and the feed's seen items — deleting and reconnecting a feed would republish the blog's whole back catalogue on the next sweep.\n\nWhat it does not touch: `enabled` (reconnecting never switches on a disabled integration, because that consumes plan allowance) and the creation date. `error_code` is cleared, so the background jobs pick it up again. `name` is kept unless the connection now points somewhere else (another Google account, another feed URL), in which case the new one is taken.\n\nNo allowance check: nothing new is created, so it works even with the plan full.\n\nRequires the `integrations:update` permission.",
        "operationId": "reconnectIntegration",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IntegrationsidOrganization"
          },
          {
            "$ref": "#/components/parameters/IntegrationsidIntegration"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/IntegrationsGoogleDriveConnectRequest"
                  },
                  {
                    "$ref": "#/components/schemas/IntegrationsRssConnectRequest"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Integration reconnected",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "integration": {
                      "$ref": "#/components/schemas/IntegrationsIntegration"
                    }
                  },
                  "required": [
                    "integration"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `2200` | Integration not found. |\n| `2201` | The `provider` in the body is not the one of this integration, or the payload is missing what that provider needs. |\n| `2203` | The provider refused the new credentials. |\n| `2205` | Feed URL unreachable or not a valid feed. |\n| `2206` | Feed has no target accounts configured. |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/organizations/{id_organization}/integrations/{id_integration}/picker_config": {
      "get": {
        "tags": [
          "integrations"
        ],
        "summary": "Short-lived configuration for the provider's own file picker",
        "description": "Everything the browser needs to open Google's Picker: a fresh access token for the connected account (scope `drive.file`, ~1 hour), the browser API key and the project number that acts as the Picker's `appId`.\n\nBehind `files:create` and **not** `files:read`: whoever cannot put files into the library has no use for a Drive token.\n\nReturns `2203` if the connection needs reconnecting, and `2201` if the provider has no picker.",
        "operationId": "getIntegrationPickerConfig",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IntegrationsidOrganization"
          },
          {
            "$ref": "#/components/parameters/IntegrationsidIntegration"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "access_token": {
                      "type": "string"
                    },
                    "expires_in": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "developer_key": {
                      "type": "string"
                    },
                    "app_id": {
                      "type": "string",
                      "description": "Google Cloud project NUMBER. With scope drive.file the permission over a picked file is granted to the project that picked it, so this must belong to the same project as the OAuth client."
                    }
                  },
                  "required": [
                    "access_token",
                    "expires_in",
                    "developer_key",
                    "app_id"
                  ],
                  "description": "Short-lived configuration for the provider's own file picker. **`google_drive` only**: any other provider answers `ERROR_CODE_2201`. It carries a live access token, so it is never cached and never logged."
                }
              }
            }
          },
          "400": {
            "description": "Error. Classify by `code` in the body, never by the HTTP status: every domain error travels with HTTP 400.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/organizations/{id_organization}/unread_messages": {
      "get": {
        "tags": [
          "messages"
        ],
        "summary": "Unread messages in the organization",
        "description": "How many incoming messages are still unread across every account of the organization. It is the number for a badge, and it costs one query: do not compute it by walking the conversations.",
        "operationId": "getUnreadMessages",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "x-planvortex-plan": "paid",
        "parameters": [
          {
            "$ref": "#/components/parameters/MessagesidOrganization"
          }
        ],
        "responses": {
          "200": {
            "description": "Unread count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "total"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/MessagesError"
          }
        }
      }
    },
    "/organizations/{id_organization}/accounts/{id_account}/conversations": {
      "get": {
        "tags": [
          "messages"
        ],
        "summary": "Conversations of an account",
        "description": "One entry per contact that has talked with this account, most recent first, with the contact already resolved and its unread count.\n\n**The first call can be slow.** If PlanVortex has no message stored for the account yet, it goes to the network, imports the recent conversations and their messages, and only then answers. From then on it reads from our database.",
        "operationId": "getConversations",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "x-planvortex-plan": "paid",
        "parameters": [
          {
            "$ref": "#/components/parameters/MessagesidOrganization"
          },
          {
            "$ref": "#/components/parameters/MessagesidAccount"
          },
          {
            "$ref": "#/components/parameters/Messagesoffset"
          },
          {
            "$ref": "#/components/parameters/Messageslimit"
          }
        ],
        "responses": {
          "200": {
            "description": "Conversations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MessagesConversation"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Conversations this account has in total, ignoring the pagination."
                    }
                  },
                  "required": [
                    "conversations",
                    "total"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/MessagesError"
          }
        }
      }
    },
    "/organizations/{id_organization}/accounts/{id_account}/conversations_total": {
      "get": {
        "tags": [
          "messages"
        ],
        "summary": "Conversation counts of one account",
        "description": "How many conversations the account had in a date range, either as a single number or grouped into a series.\n\nA *conversation* here is one participant on one day: the same contact writing three times in an afternoon counts once, and counts again tomorrow. It is the unit the plan is measured in, not the number of messages.",
        "operationId": "getAccountConversationsTotal",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "x-planvortex-plan": "paid",
        "parameters": [
          {
            "$ref": "#/components/parameters/MessagesidOrganization"
          },
          {
            "$ref": "#/components/parameters/MessagesidAccount"
          },
          {
            "$ref": "#/components/parameters/MessagesfromDate"
          },
          {
            "$ref": "#/components/parameters/MessagestoDate"
          },
          {
            "$ref": "#/components/parameters/MessagesgroupBy"
          }
        ],
        "responses": {
          "200": {
            "description": "Conversation counts",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessagesConversationTotals"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/MessagesError"
          }
        }
      }
    },
    "/organizations/{id_organization}/conversations_total": {
      "get": {
        "tags": [
          "messages"
        ],
        "summary": "Conversation counts of the whole organization",
        "description": "The same count as the per-account endpoint, adding up every account of the organization. Use it for the plan-consumption screen; use the per-account one to compare channels.",
        "operationId": "getOrganizationConversationsTotal",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "x-planvortex-plan": "paid",
        "parameters": [
          {
            "$ref": "#/components/parameters/MessagesidOrganization"
          },
          {
            "$ref": "#/components/parameters/MessagesfromDate"
          },
          {
            "$ref": "#/components/parameters/MessagestoDate"
          },
          {
            "$ref": "#/components/parameters/MessagesgroupBy"
          }
        ],
        "responses": {
          "200": {
            "description": "Conversation counts",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessagesConversationTotals"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/MessagesError"
          }
        }
      }
    },
    "/organizations/{id_organization}/accounts/{id_account}/messages/{id_contact}": {
      "get": {
        "tags": [
          "messages"
        ],
        "summary": "Messages exchanged with one contact",
        "description": "The thread with one contact, newest first.\n\n**Reading the first page marks the thread as read** (`offset=0` only), which is what makes `unread_messages` go down. If PlanVortex has nothing stored for that contact it imports the thread from the network first.",
        "operationId": "getMessages",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "x-planvortex-plan": "paid",
        "parameters": [
          {
            "$ref": "#/components/parameters/MessagesidOrganization"
          },
          {
            "$ref": "#/components/parameters/MessagesidAccount"
          },
          {
            "$ref": "#/components/parameters/MessagesidContact"
          },
          {
            "$ref": "#/components/parameters/Messagesoffset"
          },
          {
            "$ref": "#/components/parameters/Messageslimit"
          }
        ],
        "responses": {
          "200": {
            "description": "Messages",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "messages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Message"
                      }
                    },
                    "total": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "messages",
                    "total"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/MessagesError"
          }
        }
      },
      "post": {
        "tags": [
          "messages"
        ],
        "summary": "Send a message to a contact",
        "description": "Writes to the contact through the account's network.\n\nWhat is valid depends on `message_type` and on the network:\n\n• `simple_message` needs `text`, and it is validated against `characters` in `GET /social_limits`.\n• **WhatsApp only allows a free-form message inside 24 hours** of the contact's last message. Outside that window the only thing that goes through is a `template_message` with a pre-approved template, and anything else answers an error.\n• `file_message` needs at least one entry in `message_options.files` (an upload of this organization) or in `files_urls`.\n• Facebook and Instagram accept **one** file per message.\n• `quick_reply_message`, `button_message`, `elements_message` and `interactive_message` are Meta-only shapes and each one requires its own block inside `message_options`.",
        "operationId": "createMessage",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "x-planvortex-plan": "paid",
        "parameters": [
          {
            "$ref": "#/components/parameters/MessagesidOrganization"
          },
          {
            "$ref": "#/components/parameters/MessagesidAccount"
          },
          {
            "$ref": "#/components/parameters/MessagesidContact"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MessagesMessageInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The message that was sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/Message"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/MessagesError"
          }
        }
      }
    },
    "/organizations/{id_organization}/accounts/{id_account}/messages": {
      "delete": {
        "tags": [
          "messages"
        ],
        "summary": "Delete every message of an account",
        "description": "Removes the whole message history PlanVortex stores for this account. **It does not delete anything on the social network**: the conversation is still there, and reading the account again re-imports it.",
        "operationId": "deleteMessagesByAccount",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "x-planvortex-plan": "paid",
        "parameters": [
          {
            "$ref": "#/components/parameters/MessagesidOrganization"
          },
          {
            "$ref": "#/components/parameters/MessagesidAccount"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/MessagesError"
          }
        }
      }
    },
    "/organizations/{id_organization}/accounts/{id_account}/message_templates": {
      "get": {
        "tags": [
          "message templates"
        ],
        "summary": "List the account's message templates",
        "description": "The templates the **network** has approved for this account. They are read live from the network, not from our database, so a template approved a minute ago shows up here.\n\nThe shape of a template is the network's own: PlanVortex does not normalise it.\n\n**WhatsApp only.** No other network implements message templates: asking any of them returns HTTP 500 with `code: 500` (an unhandled server error), not the 1502 you would expect. Check `messages` in `GET /social_capabilities` and that the account is WhatsApp before calling.",
        "operationId": "getMessageTemplates",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "x-planvortex-plan": "paid",
        "parameters": [
          {
            "$ref": "#/components/parameters/MessagesidOrganization"
          },
          {
            "$ref": "#/components/parameters/MessagesidAccount"
          },
          {
            "$ref": "#/components/parameters/Messagesoffset"
          },
          {
            "$ref": "#/components/parameters/Messageslimit"
          }
        ],
        "responses": {
          "200": {
            "description": "Templates",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "templates": {
                      "type": "array",
                      "description": "Templates as the network returns them.",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    },
                    "total": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "templates",
                    "total"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/MessagesError"
          }
        }
      },
      "post": {
        "tags": [
          "message templates"
        ],
        "summary": "Create a message template",
        "description": "Sends a template to the network for approval. The body travels to the network as it is, so its fields are the network's (for WhatsApp: `name`, `language`, `category`, `components`).\n\n**Approval is not immediate.** The template comes back in a pending state and only becomes usable when the network approves it, which takes minutes or hours and can be refused.\n\n**WhatsApp only.** No other network implements message templates: asking any of them returns HTTP 500 with `code: 500` (an unhandled server error), not the 1502 you would expect. Check `messages` in `GET /social_capabilities` and that the account is WhatsApp before calling.",
        "operationId": "createMessageTemplate",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "x-planvortex-plan": "paid",
        "parameters": [
          {
            "$ref": "#/components/parameters/MessagesidOrganization"
          },
          {
            "$ref": "#/components/parameters/MessagesidAccount"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "The template, in the network's own format.",
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created template",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "template": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  },
                  "required": [
                    "template"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/MessagesError"
          }
        }
      },
      "delete": {
        "tags": [
          "message templates"
        ],
        "summary": "Delete a message template",
        "description": "Deletes the template on the network. **The template is identified by query parameters, not by a path segment**, and WhatsApp needs both the id and the name.\n\n**WhatsApp only.** No other network implements message templates: asking any of them returns HTTP 500 with `code: 500` (an unhandled server error), not the 1502 you would expect. Check `messages` in `GET /social_capabilities` and that the account is WhatsApp before calling.",
        "operationId": "deleteMessageTemplate",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "x-planvortex-plan": "paid",
        "parameters": [
          {
            "$ref": "#/components/parameters/MessagesidOrganization"
          },
          {
            "$ref": "#/components/parameters/MessagesidAccount"
          },
          {
            "name": "template_id",
            "in": "query",
            "description": "Identifier of the template on the network.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "template_name",
            "in": "query",
            "description": "Name of the template on the network.",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/MessagesError"
          }
        }
      }
    },
    "/organizations/{id_organization}": {
      "get": {
        "tags": [
          "orgnaizations"
        ],
        "summary": "Get an organization by identifier",
        "description": "Get one organization by its identifier",
        "operationId": "getOrganization",
        "x-planvortex-identity": [
          "current_user",
          "current_app",
          "temporal_token"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "getUse",
            "in": "query",
            "description": "Determine if the query will return the actual use of the organization plan",
            "required": false,
            "explode": true,
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationsOrganizationOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      },
      "put": {
        "tags": [
          "orgnaizations"
        ],
        "summary": "Update a child organization by identifier",
        "description": "Update child organization by its identifier",
        "operationId": "updateOrganizationInsideOrganization",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationsOrganizationOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrganizationsOrganizationUpdate"
              },
              "examples": {
                "changePlan": {
                  "summary": "Reassign resources",
                  "value": {
                    "actual_plan": {
                      "accounts": 8,
                      "publications": 300,
                      "users": 3,
                      "space": 1,
                      "integrations": 3,
                      "twitter_credits": 5000,
                      "ai_credits": 4000
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "orgnaizations"
        ],
        "summary": "Delete a child organization by identifier",
        "description": "Delete child organization by its identifier",
        "operationId": "deleteChildOrganization",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/organizations/{id_organization}/limits": {
      "get": {
        "tags": [
          "orgnaizations"
        ],
        "summary": "Retrieve actual limitations for the received organization",
        "description": "etrieve actual limitations for the received organization",
        "operationId": "getOrganizationLimits",
        "x-planvortex-identity": [
          "current_user",
          "current_app",
          "temporal_token"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationsLimit"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/organizations/{id_organization}/users": {
      "get": {
        "tags": [
          "orgnaizations"
        ],
        "summary": "Retrieve all users in the received organization",
        "description": "Retrieve all users with any role in the received organization",
        "operationId": "getUserInOrganizationRole",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationsUserList"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/organizations/{id_organization}/organizations": {
      "get": {
        "tags": [
          "orgnaizations"
        ],
        "summary": "Get child organizations of the received organization",
        "description": "Return a list of child organizations inside the received organization",
        "operationId": "getChildOrganizations",
        "x-planvortex-identity": [
          "current_user",
          "current_app",
          "temporal_token"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "getUse",
            "in": "query",
            "description": "Boolean determine if the server will response with the actual use of each client",
            "required": false,
            "explode": true,
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Number of records from which you will get clients (pagination)",
            "required": false,
            "explode": true,
            "schema": {
              "type": "integer",
              "default": "0"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "The limit of records will be retrieved (pagination)",
            "required": false,
            "explode": true,
            "schema": {
              "type": "integer",
              "default": "10"
            }
          },
          {
            "name": "name",
            "in": "query",
            "description": "The limit of records will be retrieved (pagination)",
            "required": false,
            "explode": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationsOrganizationList"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      },
      "post": {
        "tags": [
          "orgnaizations"
        ],
        "summary": "Create a child organization inside the received organization",
        "description": "Create a new child organization with its own resources inside the received parent organization",
        "operationId": "createOrganizationInsideOrganization",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Parent organization. The new organization hangs from this one",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationsOrganizationOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `1107` | Organizations doesn't allow set parent_organization as his own _id |\n| `1100` | Max deep of parents allowed |\n| `1400` | Account limit reached for the organization plan |\n| `1401` | Publication limit reached for the organization plan |\n| `1402` | Users limit reached for the organization plan |\n| `1403` | Shared space limit reached for the organization plan |\n| `1407` | Twitter credits limit reached for the organization plan. The sum of twitter_credits assigned to the client's organizations cannot exceed the client's plan twitter_credits. |\n| `1408` | AI credits limit reached for the organization plan. The sum of ai_credits assigned to the client's organizations cannot exceed the client's plan ai_credits. |\n| `1301` | Client plan invalid. Min accounts must be X |\n| `1302` | Client plan invalid. Min publications must be X |\n| `1303` | Client plan invalid. Min users must be at least X |\n| `1306` | Client plan invalid. Min shared space must be at least X |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "requestBody": {
          "required": true,
          "description": "Creates a child organization. `parent_organization` is taken from the path, so sending it in the body has no effect.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrganizationsOrganizationCreate"
              },
              "examples": {
                "basic": {
                  "summary": "Create a child organization",
                  "value": {
                    "name": "Client X - Spain",
                    "actual_plan": {
                      "accounts": 3,
                      "publications": 100,
                      "users": 1,
                      "space": 0.25,
                      "integrations": 0,
                      "twitter_credits": 0,
                      "ai_credits": 0
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/organizations/{id_organization}/social_credentials/{social_network}": {
      "put": {
        "tags": [
          "orgnaizations"
        ],
        "summary": "Save the organization's own application credentials for a network",
        "description": "**Bring your own bot.** On every network but one, the application talking to the social network is PlanVortex's. On `discord` it is the client's: the organization creates its own application in Discord's developer portal and saves its credentials here.\n\nThe reason is not technical. The permission that lets an application read the **text** of messages (`MESSAGE_CONTENT`) is reviewed **per application** once it is reachable by more than 10.000 users, so one shared application would drag every client on the platform into an annual review the first time a single community grew. This way the exposure stays with the client that caused it.\n\nThree consequences worth building against:\n\n• **Until this is saved, `discord` does not appear in `GET /organizations/{id_organization}/connect_links` at all** and every Discord call answers error 960. That is the initial state of every organization, not a failure.\n• **Secrets are write-only.** They are stored encrypted and never come back: the organization object returns `client_id`, `application_name`, `verified_date` and the two booleans `has_client_secret` / `has_bot_token`, never the values. **What you do not send is kept**, so the `client_id` can be corrected without resending the token.\n• **Saving validates against Discord first** (`GET /oauth2/applications/@me`) and stores nothing if that fails. It also catches the two portal switches that would otherwise break the OAuth flow with no explanation: *Public Bot* must be **on** and *Requires OAuth2 Code Grant* must be **off**.\n\nRequires the `organization:update` permission (`client_organization:update` for apps).\n\nThere is no GET on purpose: everything readable already travels inside the normal organization object, in `social_credentials`.",
        "operationId": "updateOrganizationSocialCredentials",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "social_network",
            "in": "path",
            "required": true,
            "description": "The network the credentials belong to. Only `discord` has its own application today; any other value answers error 702.",
            "schema": {
              "type": "string",
              "enum": [
                "discord"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrganizationsSocialCredentialsInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The organization, with the credentials it now has. Never with their values.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationsOrganizationOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `702` | That network has no own-application credentials |\n| `1406` | A required field is missing: `client_id`, `client_secret` or `bot_token` |\n| `959` | Discord refused the credentials, or the application is misconfigured in the portal |\n| `1101` | Invalid organization |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      },
      "delete": {
        "tags": [
          "orgnaizations"
        ],
        "summary": "Remove the organization's own credentials for a network",
        "description": "The organization stops being able to connect accounts on that network, and the network disappears from `connect_links` again.\n\n**Accounts already connected are not touched.** Publishing through a Discord channel goes out through that channel's incoming webhook, which does not depend on these credentials — what stops working is reading comments and statistics, which needs the bot.",
        "operationId": "deleteOrganizationSocialCredentials",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "social_network",
            "in": "path",
            "required": true,
            "description": "The network whose credentials are removed.",
            "schema": {
              "type": "string",
              "enum": [
                "discord"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The organization without those credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationsOrganizationOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `702` | That network has no own-application credentials |\n| `1101` | Invalid organization |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/organizations/{id_organization}/temporal_connect_token": {
      "get": {
        "tags": [
          "orgnaizations"
        ],
        "summary": "Issue a temporal connect token for an end user",
        "description": "Returns a URL that lets **a person** connect a social account to this organization, without that person having a PlanVortex account and without your `client_secret` ever leaving your server.\n\nIt exists because **an app cannot connect accounts**: `connect_links` and `account-connect` refuse app credentials with error 519, since authorizing Instagram is an OAuth flow with a human in front of it. The shape of the integration is therefore:\n\n1. Your server asks for this token — **this endpoint is the one that requires app credentials**, and a user token is refused. So is a temporal connect token: it cannot renew itself, which is what keeps its life a real limit rather than the first link of a chain.\n2. You send your end user to the `url` that comes back, by redirect or in an iframe. The bare\n   `token` comes back too, so a server-side client can authenticate with it directly instead of\n   parsing it out of that URL.\n3. With that token the browser completes `connect_links` and `account-connect`.\n\nThe token lasts **fifteen minutes**, is tied to **this** organization — using it against another answers error 1101 — and carries only two permissions: create accounts and read the organization. It is the piece designed for a browser, and the only credential of ours that belongs there.\n\n**It connects once.** As soon as an `account-connect` succeeds, the token stops being able to connect anything else and answers error 543; the `enable` calls that finish that same connection keep working until it expires. Issue a new one per connection — they are free and immediate.\n\n**If you pass `social_network`, the token is bound to that network** and will not connect any other (error 544). Leave it out to let the person pick.\n\nIf you send `redirect_uri` it has to be one of the app's `redirect_urls`, or the call answers error 532.",
        "operationId": "createTemporalConnectToken",
        "x-planvortex-identity": [
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "social_network",
            "in": "query",
            "required": false,
            "description": "Network the user is going to connect. It travels inside the returned URL **and inside the token itself**, so the token will only connect that network (error 544 otherwise). A network outside the allowed list is rejected here with error 702.",
            "schema": {
              "$ref": "#/components/schemas/SocialNetwork"
            }
          },
          {
            "name": "redirect_uri",
            "in": "query",
            "required": false,
            "description": "Where the user comes back after connecting. Has to be one of the app's `redirect_urls` (error 532).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The URL to send the user to, the bare token, and when it expires.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "description": "Ready to redirect to. Carries the token in its `token` query parameter, and the network in `social_network` when one was asked for."
                    },
                    "token": {
                      "type": "string",
                      "description": "The same token, on its own. This is what you pass to a client that authenticates with a temporal token; do not parse it out of `url`."
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When the token stops working. Fifteen minutes after it was issued."
                    }
                  },
                  "required": [
                    "url",
                    "token",
                    "expires_at"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `514` | This endpoint needs app credentials: neither a user token nor a temporal connect token can issue one |\n| `532` | `redirect_uri` is not one of the app's registered `redirect_urls` |\n| `702` | `social_network` is not one of the networks PlanVortex supports |\n| `1101` | Invalid organization |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/organizations/{id_organization}/ai-context": {
      "put": {
        "tags": [
          "orgnaizations"
        ],
        "summary": "Replace the organization's brand context for AI",
        "description": "The brand context every AI plan of this organization starts from: what the business does, who it talks to, the tone, what must never appear.\n\n**It replaces the whole context, it is not a patch.** A field you leave out is erased, and an empty body removes the context entirely — after which plans run on their own prompt alone.\n\nThere is no GET: the current context already travels inside the organization object, in `ai_context`.",
        "operationId": "updateOrganizationAiContext",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AiContext"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The organization with its new context",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationsOrganizationOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/organizations/{id_organization}/roles": {
      "get": {
        "tags": [
          "organization_roles"
        ],
        "summary": "List the roles of an organization",
        "description": "Returns the roles defined in the organization, paginated.\n\nAccess: the caller needs either the client-level permission `client_organization_roles:read` or the organization-level permission `organization_roles:read`. Every endpoint under this organization also requires the client plan to allow at least 2 users (error `511` otherwise).",
        "operationId": "getOrganizationRoles",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/OrganizationsRolesidOrganization"
          },
          {
            "$ref": "#/components/parameters/OrganizationsRolesoffset"
          },
          {
            "$ref": "#/components/parameters/OrganizationsRoleslimit"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationsRolesOrganizationRoleList"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `501` | Invalid authorization token |\n| `511` | This functionality requires a plan with at least 2 users |\n| `518` | User or application deleted |\n| `522` | Invalid token: expired |\n| `1101` | Invalid organization |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Insufficient permissions. The body carries error code `520` with the permissions that were required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server (PlanVortex error code `500`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "organization_roles"
        ],
        "summary": "Create a role in an organization",
        "description": "Creates a role with its permission list. The name must be unique within the organization (error `1205`) and every permission must be a valid organization permission (error `1201`).\n\nAccess: the caller needs either the client-level permission `client_organization_roles:create` or the organization-level permission `organization_roles:create`. Every endpoint under this organization also requires the client plan to allow at least 2 users (error `511` otherwise).",
        "operationId": "createOrganizationRole",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/OrganizationsRolesidOrganization"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrganizationsRolesOrganizationRoleInput"
              },
              "examples": {
                "editor": {
                  "summary": "A role that can create and schedule publications",
                  "value": {
                    "name": "Editor",
                    "permissions": [
                      "organization:read",
                      "accounts:read",
                      "files:create",
                      "publications:create",
                      "publications:update",
                      "publications:read"
                    ]
                  }
                },
                "readonly": {
                  "summary": "A read-only role",
                  "value": {
                    "name": "Viewer",
                    "permissions": [
                      "organization:read",
                      "accounts:read",
                      "publications:read",
                      "publication_stats:read",
                      "account_stats:read"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationsRolesOrganizationRoleOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `501` | Invalid authorization token |\n| `511` | This functionality requires a plan with at least 2 users |\n| `518` | User or application deleted |\n| `522` | Invalid token: expired |\n| `1101` | Invalid organization |\n| `1201` | Invalid organization permission detected |\n| `1205` | Role name already exists in this organization |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Insufficient permissions. The body carries error code `520` with the permissions that were required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server (PlanVortex error code `500`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/organizations/{id_organization}/roles/{id_rol}": {
      "put": {
        "tags": [
          "organization_roles"
        ],
        "summary": "Update a role",
        "description": "Replaces the name and the permission list of the role. **Both properties are overwritten**, so send the complete permission list and not just the ones you are adding.\n\nDefault roles cannot be updated (error `1207`).\n\nAccess: the caller needs either the client-level permission `client_organization_roles:update` or the organization-level permission `organization_roles:update`. Every endpoint under this organization also requires the client plan to allow at least 2 users (error `511` otherwise).",
        "operationId": "updateOrganizationRole",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/OrganizationsRolesidOrganization"
          },
          {
            "$ref": "#/components/parameters/OrganizationsRolesidRol"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrganizationsRolesOrganizationRoleInput"
              },
              "examples": {
                "editor": {
                  "summary": "A role that can create and schedule publications",
                  "value": {
                    "name": "Editor",
                    "permissions": [
                      "organization:read",
                      "accounts:read",
                      "files:create",
                      "publications:create",
                      "publications:update",
                      "publications:read"
                    ]
                  }
                },
                "readonly": {
                  "summary": "A read-only role",
                  "value": {
                    "name": "Viewer",
                    "permissions": [
                      "organization:read",
                      "accounts:read",
                      "publications:read",
                      "publication_stats:read",
                      "account_stats:read"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationsRolesOrganizationRoleOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `501` | Invalid authorization token |\n| `511` | This functionality requires a plan with at least 2 users |\n| `518` | User or application deleted |\n| `522` | Invalid token: expired |\n| `1101` | Invalid organization |\n| `1201` | Invalid organization permission detected |\n| `1202` | Invalid organization role |\n| `1205` | Role name already exists in this organization |\n| `1207` | Default roles cannot be updated or deleted |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Insufficient permissions. The body carries error code `520` with the permissions that were required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server (PlanVortex error code `500`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "organization_roles"
        ],
        "summary": "Delete a role",
        "description": "Deletes the role. Users that belonged to it lose the permissions it granted. Default roles cannot be deleted (error `1207`).\n\nAccess: the caller needs either the client-level permission `client_organization_roles:delete` or the organization-level permission `organization_roles:delete`. Every endpoint under this organization also requires the client plan to allow at least 2 users (error `511` otherwise).",
        "operationId": "deleteOrganizationRole",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/OrganizationsRolesidOrganization"
          },
          {
            "$ref": "#/components/parameters/OrganizationsRolesidRol"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `501` | Invalid authorization token |\n| `511` | This functionality requires a plan with at least 2 users |\n| `518` | User or application deleted |\n| `522` | Invalid token: expired |\n| `1101` | Invalid organization |\n| `1202` | Invalid organization role |\n| `1207` | Default roles cannot be updated or deleted |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Insufficient permissions. The body carries error code `520` with the permissions that were required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server (PlanVortex error code `500`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/organizations/{id_organization}/roles/{id_rol}/users": {
      "get": {
        "tags": [
          "organization_roles"
        ],
        "summary": "List the users of a role",
        "description": "Returns the users that belong to the role, with the roles each of them holds in this organization. Paginated.\n\nA user that no longer exists in the identity provider is skipped instead of breaking the listing, so `users` may be shorter than `total`.\n\nAccess: the caller needs either the client-level permission `client_organization_users_roles:read` or the organization-level permission `organization_users_roles:read`. Every endpoint under this organization also requires the client plan to allow at least 2 users (error `511` otherwise).",
        "operationId": "getUsersInOrganizationRole",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/OrganizationsRolesidOrganization"
          },
          {
            "$ref": "#/components/parameters/OrganizationsRolesidRol"
          },
          {
            "$ref": "#/components/parameters/OrganizationsRolesoffset"
          },
          {
            "$ref": "#/components/parameters/OrganizationsRoleslimit"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationsRolesUserInOrganizationList"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `501` | Invalid authorization token |\n| `511` | This functionality requires a plan with at least 2 users |\n| `518` | User or application deleted |\n| `522` | Invalid token: expired |\n| `1101` | Invalid organization |\n| `1202` | Invalid organization role |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Insufficient permissions. The body carries error code `520` with the permissions that were required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server (PlanVortex error code `500`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/organizations/{id_organization}/roles/{id_rol}/users/{id_user}": {
      "post": {
        "tags": [
          "organization_roles"
        ],
        "summary": "Add an existing user to a role",
        "description": "Adds a user who already exists to the role. Idempotent: adding the same user twice does nothing.\n\nThe organization cannot exceed the number of users its plan allows (error `1109`). To bring in somebody who is not a PlanVortex user yet, use the invite endpoint instead.\n\nAccess: the caller needs either the client-level permission `client_organization_users_roles:create` or the organization-level permission `organization_users_roles:create`. Every endpoint under this organization also requires the client plan to allow at least 2 users (error `511` otherwise).",
        "operationId": "addUserToOrganizationRole",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/OrganizationsRolesidOrganization"
          },
          {
            "$ref": "#/components/parameters/OrganizationsRolesidRol"
          },
          {
            "$ref": "#/components/parameters/OrganizationsRolesidUser"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `501` | Invalid authorization token |\n| `511` | This functionality requires a plan with at least 2 users |\n| `518` | User or application deleted |\n| `522` | Invalid token: expired |\n| `1101` | Invalid organization |\n| `1109` | Max users per organization reached |\n| `1202` | Invalid organization role |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Insufficient permissions. The body carries error code `520` with the permissions that were required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server (PlanVortex error code `500`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "organization_roles"
        ],
        "summary": "Remove a user from a role",
        "description": "Removes the user from the role. The last user of a default role can be removed too: an organization's `Admin` role is allowed to end up with nobody in it.\n\nAccess: the caller needs either the client-level permission `client_organization_users_roles:delete` or the organization-level permission `organization_users_roles:delete`. Every endpoint under this organization also requires the client plan to allow at least 2 users (error `511` otherwise).",
        "operationId": "removeUserFromOrganizationRole",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/OrganizationsRolesidOrganization"
          },
          {
            "$ref": "#/components/parameters/OrganizationsRolesidRol"
          },
          {
            "$ref": "#/components/parameters/OrganizationsRolesidUser"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `501` | Invalid authorization token |\n| `511` | This functionality requires a plan with at least 2 users |\n| `518` | User or application deleted |\n| `522` | Invalid token: expired |\n| `1101` | Invalid organization |\n| `1202` | Invalid organization role |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Insufficient permissions. The body carries error code `520` with the permissions that were required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server (PlanVortex error code `500`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/organizations/{id_organization}/roles/{id_rol}/invite": {
      "post": {
        "tags": [
          "organization_roles"
        ],
        "summary": "Invite somebody to a role by email",
        "description": "Sends an invitation email with a temporary link and assigns the invited user to the role.\n\nIf nobody exists with that address, a disabled user is created and enabled when the invitation is accepted. If the address is already a disabled invited user, the invitation is re-sent.\n\nReturns `609` if the address is not valid and `508` if the email could not be sent.\n\nAccess: the caller needs either the client-level permission `client_organization_roles:create` or the organization-level permission `organization_roles:create`. Every endpoint under this organization also requires the client plan to allow at least 2 users (error `511` otherwise).",
        "operationId": "inviteUserToOrganizationRole",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/OrganizationsRolesidOrganization"
          },
          {
            "$ref": "#/components/parameters/OrganizationsRolesidRol"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "Address to invite."
                  }
                }
              },
              "examples": {
                "invite": {
                  "summary": "Invite somebody",
                  "value": {
                    "email": "someone@example.com"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `501` | Invalid authorization token |\n| `508` | Error sending invitation |\n| `511` | This functionality requires a plan with at least 2 users |\n| `518` | User or application deleted |\n| `522` | Invalid token: expired |\n| `609` | Invalid email |\n| `1101` | Invalid organization |\n| `1202` | Invalid organization role |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Insufficient permissions. The body carries error code `520` with the permissions that were required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server (PlanVortex error code `500`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/organizations_permissions": {
      "get": {
        "tags": [
          "organization_roles"
        ],
        "summary": "List every organization permission that exists",
        "description": "Returns the catalogue of organization permissions accepted in the `permissions` property of a role. Static: it does not depend on the caller or on any organization. Only a valid token is required.",
        "operationId": "getOrganizationPermissions",
        "x-planvortex-identity": [
          "current_user",
          "current_app",
          "temporal_token"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrganizationsRolesPermissionList"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `501` | Invalid authorization token |\n| `518` | User or application deleted |\n| `522` | Invalid token: expired |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Insufficient permissions. The body carries error code `520` with the permissions that were required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server (PlanVortex error code `500`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/organizations/{id_organization}/accounts/{id_account}/products_catalogs": {
      "get": {
        "tags": [
          "products"
        ],
        "summary": "List the account's product catalogues",
        "description": "The catalogues owned by the business behind this account.\n\n**`total` is the size of the page, not of the collection.** Meta paginates with cursors and does not return a count, so there is nothing better to report: keep asking with a larger `offset` until a page comes back short.\n\n**Facebook and Instagram only.** They are the only two networks with `products: true` in `GET /social_capabilities`; WhatsApp does not have it either, despite its own catalogue. Everything here speaks Meta Commerce, with Meta's own field names.",
        "operationId": "getProductCatalogs",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ProductsidOrganization"
          },
          {
            "$ref": "#/components/parameters/ProductsidAccount"
          },
          {
            "$ref": "#/components/parameters/Productsoffset"
          },
          {
            "$ref": "#/components/parameters/Productslimit"
          }
        ],
        "responses": {
          "200": {
            "description": "Catalogues",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ProductsProductCatalog"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "How many catalogues came back **in this page**, not how many exist. It is the length of `items`, so it can never tell you there is another page."
                    }
                  },
                  "required": [
                    "items",
                    "total"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ProductsError"
          }
        }
      },
      "post": {
        "tags": [
          "products"
        ],
        "summary": "Create a product catalogue",
        "description": "Creates a catalogue under the business behind this account. Only `name` is required.\n\n**The answer carries the identifier, not the object**, even though the field is called `product_catalog`. Read the list back if you need the full catalogue.\n\n**Facebook and Instagram only.** They are the only two networks with `products: true` in `GET /social_capabilities`; WhatsApp does not have it either, despite its own catalogue. Everything here speaks Meta Commerce, with Meta's own field names.",
        "operationId": "createProductCatalog",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ProductsidOrganization"
          },
          {
            "$ref": "#/components/parameters/ProductsidAccount"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProductsProductCatalogInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Identifier of the created catalogue",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "product_catalog": {
                      "type": "string",
                      "description": "Identifier of the catalogue on the network. **A string, not the catalogue**: the name says otherwise and it is the id. Read it back with `GET .../products_catalogs` if you need the rest."
                    }
                  },
                  "required": [
                    "product_catalog"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ProductsError"
          }
        }
      }
    },
    "/organizations/{id_organization}/accounts/{id_account}/products": {
      "get": {
        "tags": [
          "products"
        ],
        "summary": "List products",
        "description": "The products of a catalogue, or a single product when `product_id` is sent. As with the catalogues, `total` counts the page.\n\n**Facebook and Instagram only.** They are the only two networks with `products: true` in `GET /social_capabilities`; WhatsApp does not have it either, despite its own catalogue. Everything here speaks Meta Commerce, with Meta's own field names.",
        "operationId": "getProducts",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ProductsidOrganization"
          },
          {
            "$ref": "#/components/parameters/ProductsidAccount"
          },
          {
            "name": "product_catalog_id",
            "in": "query",
            "description": "The catalogue to list. **Effectively required**: without it the request fails with `ERROR_CODE_2000`. Get one from `GET .../products_catalogs`.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "product_id",
            "in": "query",
            "description": "One product, by its identifier on the network. It takes precedence over `product_catalog_id`, and one of the two has to travel or the call answers `ERROR_CODE_2000`. It used to be forwarded under a name the SDK does not read, so it never reached the network; that was fixed on 2026-08-24.\n\n**The answer is shaped differently.** Asking for one product goes to that product's node, so the network answers with the product itself and `items` carries an object rather than an array. Handle both shapes, or use `product_catalog_id` and pick the product out of the page.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Productsoffset"
          },
          {
            "$ref": "#/components/parameters/Productslimit"
          }
        ],
        "responses": {
          "200": {
            "description": "Products",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ProductsProduct"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "**Always 0 today.** It is read from Meta's `summary.total_count`, and PlanVortex does not ask for the summary, so the field never arrives. Page until you get a short page instead of trusting this number."
                    }
                  },
                  "required": [
                    "items",
                    "total"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ProductsError"
          }
        }
      },
      "post": {
        "tags": [
          "products"
        ],
        "summary": "Create or update a product",
        "description": "**One endpoint does both.** Send `id` in the body and the existing product is updated; leave it out and a new product is created inside `product_catalog_id`.\n\n`product_catalog_id` travels as a **query parameter** and is required when creating. Without it the call answers error 2000.\n\n**Facebook and Instagram only.** They are the only two networks with `products: true` in `GET /social_capabilities`; WhatsApp does not have it either, despite its own catalogue. Everything here speaks Meta Commerce, with Meta's own field names.",
        "operationId": "createProduct",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ProductsidOrganization"
          },
          {
            "$ref": "#/components/parameters/ProductsidAccount"
          },
          {
            "name": "product_catalog_id",
            "in": "query",
            "description": "Catalogue the product is created in. Required unless the body carries an `id`.",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProductsProductInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Identifier of the created or updated product",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "product_id": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "product_id"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ProductsError"
          }
        }
      }
    },
    "/organizations/{id_organization}/accounts/{id_account}/publish": {
      "get": {
        "tags": [
          "publications"
        ],
        "summary": "Retrieve list of publications",
        "description": "Retrieve list of publications from one account (paginated)",
        "operationId": "getAccountPublications",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_account",
            "in": "path",
            "description": "Account identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Number of records from which you will get publications (pagination)",
            "required": false,
            "explode": true,
            "schema": {
              "type": "integer",
              "default": "0"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "The limit of records will be retrieved (pagination)",
            "required": false,
            "explode": true,
            "schema": {
              "type": "integer",
              "default": "10"
            }
          },
          {
            "name": "from_date",
            "in": "query",
            "description": "Date from which records will be returned",
            "required": false,
            "explode": true,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to_date",
            "in": "query",
            "description": "Date until which records will be returned",
            "required": false,
            "explode": true,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "String used to search results",
            "required": false,
            "explode": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "orderByPublish",
            "in": "query",
            "description": "Determine if the results will be ordered by publication_date instead of creation_date",
            "required": false,
            "explode": true,
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "social_network",
            "in": "query",
            "description": "Only publications of these networks. Repeat the parameter for more than one.",
            "required": false,
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/SocialNetwork"
              }
            }
          },
          {
            "name": "accounts",
            "in": "query",
            "description": "Retrieve publications based on a list of identifier accounts",
            "required": false,
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "state",
            "in": "query",
            "description": "Retrieve publications based on a list of states",
            "required": false,
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "ready",
                  "withErrors",
                  "sended",
                  "draft",
                  "publishing"
                ]
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicationsPublicationList"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      },
      "post": {
        "tags": [
          "publications"
        ],
        "summary": "Create new publications",
        "description": "Create new publications. For X (Twitter) accounts, paid actions consume the client's monthly X credit pool (shared across organizations). When a publication is sent immediately, the required credits are checked before publishing and only charged on success. A tweet costs 15 credits, or 200 credits if its text contains a link. Link detection uses the same rule the backend applies: an http(s):// URL, a www. host, or a bare domain with a recognized TLD (regex TWITTER_LINK_REGEX). A scheduled publication does not consume credits when created; it is charged when the background job publishes it. If there are not enough credits at publish time the scheduled publication is left in state 'withErrors' with publication_errors.code = 940 and the client-app webhook is fired (it is never thrown). Publishing immediately without enough credits returns error 940.",
        "operationId": "addPublication",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization the publication belongs to",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_account",
            "in": "path",
            "description": "Connected social account the publication will be published to",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/PublicationsPublicationInput"
                  },
                  {
                    "required": [
                      "social_network"
                    ]
                  }
                ]
              },
              "examples": {
                "publishNow": {
                  "summary": "Publish a text post immediately",
                  "value": {
                    "social_network": "linkedin",
                    "text": "Hello from my product"
                  }
                },
                "schedule": {
                  "summary": "Schedule a post with an image",
                  "value": {
                    "social_network": "instagram",
                    "text": "Coming soon",
                    "publication_type": "profile",
                    "files": [
                      "66f1a2b3c4d5e6f7a8b9c0d1"
                    ],
                    "publish_date": "2026-09-01T10:00:00.000Z"
                  }
                },
                "draft": {
                  "summary": "Create a draft without publishing",
                  "value": {
                    "social_network": "facebook",
                    "text": "Work in progress",
                    "state": "draft"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicationsPublicationOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `924` | Max publication reached per month |\n| `918` | Publications must be attached to an account. Missing property: id_account |\n| `913` | Invalid publication type |\n| `914` | Invalid publication state |\n| `923` | Social network doesn't allow this publication type |\n| `700` | Account not connected, missing authorizations tokens |\n| `915` | You should write a text or, at least, upload some image or video |\n| `940` | X (Twitter) credits exhausted. Not enough monthly credits to publish this tweet. Response data: { used, limit }. |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/organizations/{id_organization}/publish": {
      "get": {
        "tags": [
          "publications"
        ],
        "summary": "Retrieve list of publications",
        "description": "Retrieve all publications of the organization (paginated)",
        "operationId": "getPublications",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Number of records from which you will get publications (pagination)",
            "required": false,
            "explode": true,
            "schema": {
              "type": "integer",
              "default": "0"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "The limit of records will be retrieved (pagination)",
            "required": false,
            "explode": true,
            "schema": {
              "type": "integer",
              "default": "10"
            }
          },
          {
            "name": "from_date",
            "in": "query",
            "description": "Date from which records will be returned",
            "required": false,
            "explode": true,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to_date",
            "in": "query",
            "description": "Date until which records will be returned",
            "required": false,
            "explode": true,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "String used to search results",
            "required": false,
            "explode": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "orderByPublish",
            "in": "query",
            "description": "Determine if the results will be ordered by publication_date instead of creation_date",
            "required": false,
            "explode": true,
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "social_network",
            "in": "query",
            "description": "Only publications of these networks. Repeat the parameter for more than one.",
            "required": false,
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/SocialNetwork"
              }
            }
          },
          {
            "name": "accounts",
            "in": "query",
            "description": "Retrieve publications based on a list of identifier accounts",
            "required": false,
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "state",
            "in": "query",
            "description": "Retrieve publications based on a list of states",
            "required": false,
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "ready",
                  "withErrors",
                  "sended",
                  "draft",
                  "publishing"
                ]
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicationsPublicationList"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/organizations/{id_organization}/publish/{id_publication}": {
      "get": {
        "tags": [
          "publications"
        ],
        "summary": "Retrieve publication by identifier",
        "description": "Retrieve publication by identifier.\n\nA **deleted** publication answers error 917, the same as one that never existed: the delete is soft on our side, but it is a delete for the API.",
        "operationId": "getPublication",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicationsPublicationOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `917` | Publication doesn't exists |\n| `935` | Invalid publication |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "parameters": [
          {
            "name": "id_publication",
            "in": "path",
            "description": "Publication identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "put": {
        "tags": [
          "publications"
        ],
        "summary": "Update publication by identifier",
        "description": "Update publication by identifier",
        "operationId": "updatePublication",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization the publication belongs to",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_publication",
            "in": "path",
            "description": "Publication identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "Only the properties you send are updated; the rest keep their current value. `social_network` is already set on the publication, so it is optional here.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicationsPublicationInput"
              },
              "examples": {
                "editText": {
                  "summary": "Change the text",
                  "value": {
                    "text": "Updated copy"
                  }
                },
                "reschedule": {
                  "summary": "Move the publish date",
                  "value": {
                    "publish_date": "2026-09-15T09:30:00.000Z"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicationsPublicationOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `917` | Publication doesn't exists |\n| `935` | Invalid publication |\n| `921` | Can't update a publication already sended |\n| `940` | X (Twitter) credits exhausted. Not enough monthly credits to publish this tweet (same link detection and 15/200 tariff as on create). Response data: { used, limit }. |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      },
      "delete": {
        "tags": [
          "publications"
        ],
        "summary": "Delete publication by identifier",
        "description": "Delete publication by identifier. It stops being readable by identifier afterwards, so deleting twice answers error 917. For an already-sent X (Twitter) publication, removing the tweet on X is a paid action that consumes 15 X credits; it is only removed on X when there are enough credits. Returns error 940 when the X credit pool is exhausted.",
        "operationId": "deletePublication",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `917` | Publication doesn't exists |\n| `935` | Invalid publication |\n| `940` | X (Twitter) credits exhausted. Deleting the tweet on X could not be charged. Response data: { used, limit }. |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "parameters": [
          {
            "name": "id_publication",
            "in": "path",
            "description": "Publication identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/organizations/{id_organization}/publish/{id_publication}/retry": {
      "post": {
        "tags": [
          "publications"
        ],
        "summary": "Retry a failed publication",
        "description": "Sends a publication that ended in state `withErrors` to its social network again, WITHOUT changing its content.\n\nThe publication is sent during the request, so the response already carries the outcome: `state: \"sended\"` if it went out, or `state: \"withErrors\"` with the errors of THIS attempt if it failed again. A scheduled publication whose date has not arrived yet goes back to `ready` and the scheduler picks it up at its time.\n\nEvery accepted call spends one retry, even when the publication fails again — what the endpoint promises is an attempt, not a success. The limit is `max_retries`, also served by `GET /publication_limits`. Once it is spent the way to try again is to update the publication (`PUT`), which resets the counter to 0, or to create a new one.",
        "operationId": "retryPublication",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization the publication belongs to",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_publication",
            "in": "path",
            "description": "Publication identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The attempt ran. `publication.state` and `publication.publication_errors` carry the outcome.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicationsPublicationRetry"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `917` | Publication doesn't exists |\n| `935` | Invalid publication |\n| `949` | Only a publication with errors can be retried. Response data: { state }. |\n| `950` | Publication retry limit reached. Response data: { retries, max_retries }. Update the publication to reset the counter. |\n| `940` | X (Twitter) credits exhausted. Nothing was sent and no retry was spent. Response data: { used, limit }. |\n| `942` | Social network doesn't allow publications |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/organizations/{id_organization}/publish/{id_publication}/metrics": {
      "get": {
        "tags": [
          "publications"
        ],
        "summary": "Get metrics from the received publication",
        "description": "Get metrics from the received publication. Retrieve only metrics associated to the social network of the publication. For X (Twitter) publications, fetching metrics on demand is a paid owned-read that consumes 1 X credit from the client's monthly pool; returns error 940 when it is exhausted.\n\nFields the network did not report are **omitted from the response**, which is not the same as a zero — see the `PublicationStats` schema.",
        "operationId": "getPublicationMetrics",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicationStats"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `917` | Publication doesn't exists |\n| `935` | Invalid publication |\n| `940` | X (Twitter) credits exhausted. Not enough monthly credits to fetch these metrics. Response data: { used, limit }. |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "parameters": [
          {
            "name": "id_publication",
            "in": "path",
            "description": "Publication identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/organizations/{id_organization}/publish/{id_publication}/stats": {
      "get": {
        "tags": [
          "publications"
        ],
        "summary": "Get the measured history of a publication",
        "description": "Time series of everything already measured for this publication: one row per day, from oldest to newest.\n\nUnlike `/metrics`, this endpoint **does not call the social network** and therefore **never consumes X (Twitter) credits** — it only reads what the collector already stored. Use it to draw a publication's evolution; use `/metrics` only when you deliberately want a fresh read.\n\nStatistics are collected for **30 days** after publishing, on a decreasing cadence (every 6 h for the first 48 h, then daily, then weekly). Past that window `next_stats_update` is omitted and the series stops growing.\n\nEach row is the publication's **running total** at that date, not that day's increment: to get the daily delta, subtract consecutive rows. An empty `series` is a valid answer (publication just sent, or a network that reports no statistics for that content), not an error.",
        "operationId": "getPublicationStatsHistory",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicationsPublicationStatsHistory"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `917` | Publication doesn't exists |\n| `935` | Invalid publication |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "parameters": [
          {
            "name": "id_publication",
            "in": "path",
            "description": "Publication identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/organizations/{id_organization}/publications/stats": {
      "get": {
        "tags": [
          "publications"
        ],
        "summary": "List publications with their metrics",
        "description": "Publications sent in the range, each with its last known normalized metrics, ordered by the requested metric. Covers the organization **and its children**.\n\nTwo things to keep in mind when reading the response:\n\n- `summary` always covers the **whole organization**; `social_network` and `accounts` filter only the listing. Pass `summary=false` when paging so the aggregates are not recomputed on every page — they only depend on the date range.\n- Publications that have **not been measured yet** are included, with `metrics` absent. That is the difference with `/publications/top`, which can only rank what has already been measured. An absent metric is not a zero.\n\nOnly `sended` publications are listed: a draft or a scheduled publication has nothing to measure.",
        "operationId": "getPublicationsStats",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicationsPublicationsStatsList"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `1000` | Invalid `from_date` or `metric` |\n| `1001` | Invalid `to_date` |\n| `1003` | Range longer than 366 days. Response data: { max_days } |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from_date",
            "in": "query",
            "description": "Start of the range, filtered by publication date. Defaults to 30 days before `to_date`. The range cannot be longer than 366 days.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to_date",
            "in": "query",
            "description": "End of the range, filtered by publication date. Defaults to now.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "metric",
            "in": "query",
            "description": "Normalized metric the listing is ordered by, descending. Publications that lack it are not dropped: they sort last.",
            "schema": {
              "type": "string",
              "default": "engagement",
              "enum": [
                "impressions",
                "reach",
                "engagement",
                "likes",
                "comments",
                "shares",
                "saves",
                "clicks",
                "video_views",
                "profile_views"
              ]
            }
          },
          {
            "name": "social_network",
            "in": "query",
            "description": "Filter the listing by network. Repeat the parameter for several. Does not affect `summary`.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "accounts",
            "in": "query",
            "description": "Filter the listing by account identifier. Repeat the parameter for several. Does not affect `summary`.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "summary",
            "in": "query",
            "description": "Set to `false` to skip the aggregates and return only the listing page. They depend on the range alone, so paging with `summary=false` avoids recomputing them.",
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Publications per page",
            "schema": {
              "type": "integer",
              "default": 25
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Publications to skip",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ]
      }
    },
    "/organizations/{id_organization}/accounts/{id_account}/social_publications": {
      "get": {
        "tags": [
          "publications"
        ],
        "summary": "Read the account's timeline on the network",
        "description": "What the account has actually published **on the social network**, whoever published it — including posts written outside PlanVortex.\n\nThis is not the same question as `GET /organizations/{id_organization}/publish`, which lists what PlanVortex has scheduled or sent. Use this one to show the real feed; use that one to manage your own publications.\n\n**On X (Twitter) this costs credits.** Reading the timeline is billed at one credit per post read, and the budget is checked against `limit` before the call — the most it could cost — and charged afterwards for what actually came back. Ask for less than you need and you pay less.",
        "operationId": "getSocialPublications",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_account",
            "in": "path",
            "description": "Connected account identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Records to skip (pagination)",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Records per page. On X this is also the ceiling of what the call can cost.",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The timeline",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "publications": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Publication"
                      }
                    },
                    "total": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "publications",
                    "total"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1305` | The client has no X credits left |\n| `1405` | The organization has no X credits left |\n| `1101` | Invalid organization |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/organizations/{id_organization}/accounts/{id_account}/publish/{id_publication}": {
      "put": {
        "tags": [
          "publications"
        ],
        "summary": "Update a publication (from its account)",
        "description": "Update publication by identifier\n\nThis is the same operation as `PUT /organizations/{id_organization}/publish/{id_publication}`, reachable from the account. Both accept the same body and do the same thing; use whichever path you already have the identifiers for.",
        "operationId": "updatePublicationByAccount",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_account",
            "in": "path",
            "description": "Connected account identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_publication",
            "in": "path",
            "description": "Publication identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "Only the properties you send are updated; the rest keep their current value. `social_network` is already set on the publication, so it is optional here.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicationsPublicationInput"
              },
              "examples": {
                "editText": {
                  "summary": "Change the text",
                  "value": {
                    "text": "Updated copy"
                  }
                },
                "reschedule": {
                  "summary": "Move the publish date",
                  "value": {
                    "publish_date": "2026-09-15T09:30:00.000Z"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicationsPublicationOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `917` | Publication doesn't exists |\n| `935` | Invalid publication |\n| `921` | Can't update a publication already sended |\n| `940` | X (Twitter) credits exhausted. Not enough monthly credits to publish this tweet (same link detection and 15/200 tariff as on create). Response data: { used, limit }. |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      }
    },
    "/organizations/{id_organization}/uploads": {
      "get": {
        "tags": [
          "uploads"
        ],
        "summary": "Get all uploads (paginated) from received organization",
        "description": "Return a list of files from the received organization",
        "operationId": "getUploads",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Number of records from which you will get files (pagination)",
            "required": false,
            "explode": true,
            "schema": {
              "type": "integer",
              "default": "0"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "The limit of records will be retrieved (pagination)",
            "required": false,
            "explode": true,
            "schema": {
              "type": "integer",
              "default": "10"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadsUploadList"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        }
      },
      "post": {
        "tags": [
          "uploads"
        ],
        "summary": "Upload new file",
        "description": "Upload new file to the received organization",
        "operationId": "addUpload",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadsUploadOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `804` | Max folder size reached per organization |\n| `800` | Invalid image format file, only allow:  JPEG, PNG, BMP, GIF, JPG |\n| `801` | Invalid video format, only mp4 allowed |\n| `802` | Invalid image size, max allowed 5mb |\n| `803` | Invalid video size, max allowed 512mb |\n| `805` | Invalid file type, only allow videos and images |\n| `809` | Only videos con have a thumbnail image |\n| `810` | Thumbnail must be an image |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "file uploaded as multipart/formdata"
                  }
                },
                "required": [
                  "file"
                ]
              }
            }
          }
        }
      }
    },
    "/organizations/{id_organization}/uploads/import": {
      "post": {
        "tags": [
          "uploads"
        ],
        "summary": "Import files into the library from a connected integration",
        "description": "Brings files the user picked in the provider (Google Drive's own Picker) into the organization's library. See the Integrations schema for how to connect one and get the picker token.\n\nThe checks run in this order, and the order matters: format first (before touching the network — downloading 180 MB only to reject the format would be absurd), then the organization's storage quota, then a streamed download that counts the bytes as they arrive and aborts on the limit (the size the provider declares is informative and is never trusted), then HEIC→JPEG conversion where needed.\n\n**The response is deliberately partial**: `uploads` carries what got in and `errors` what did not, so picking six files and getting five plus one named failure is the normal, expected outcome — not an error.\n\nRequires the `files:create` permission (`client_organization_files:create` for apps).",
        "operationId": "importUploads",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "id_integration",
                  "files"
                ],
                "properties": {
                  "id_integration": {
                    "type": "string",
                    "description": "A connected integration of this organization whose provider has file_import: true."
                  },
                  "files": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "object",
                      "required": [
                        "external_id"
                      ],
                      "properties": {
                        "external_id": {
                          "type": "string",
                          "description": "File identifier at the provider (the Drive file id)."
                        },
                        "name": {
                          "type": "string",
                          "description": "File name as shown by the picker. Used for the visible name and to report which file failed."
                        },
                        "mime_type": {
                          "type": "string",
                          "description": "Type reported by the picker. Used to reject early; the real type of the body wins."
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Import finished. Partial by design: check `errors` even on 200.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "uploads": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Upload"
                      }
                    },
                    "errors": {
                      "type": "array",
                      "description": "One entry per file that did not get in.\n\n| Code | Meaning |\n| --- | --- |\n| `800` | Invalid image format. |\n| `801` | Invalid video format. |\n| `802` | Image over the size limit (5 MB). |\n| `803` | Video over the size limit (200 MB). |\n| `804` | Organization storage quota exhausted. |\n| `805` | Not an image or a video. |\n| `2204` | Not downloadable from the provider: a native Google file (Doc, Sheet, Slides) has no bytes to download, or the file is not reachable with the granted permission. |",
                      "items": {
                        "type": "object",
                        "properties": {
                          "external_id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "code": {
                            "type": "integer"
                          },
                          "message": {
                            "type": "string"
                          },
                          "data": {
                            "type": "object"
                          }
                        },
                        "required": [
                          "code",
                          "message"
                        ]
                      }
                    }
                  },
                  "required": [
                    "uploads",
                    "errors"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`2200` the integration does not exist or belongs to another organization; `2201` its provider does not contribute files; `807` the body carries no files."
          }
        }
      }
    },
    "/organizations/{id_organization}/uploads/{id_upload}": {
      "get": {
        "tags": [
          "uploads"
        ],
        "summary": "Retrieve a file by identifier",
        "description": "Retrieve a file by identifier",
        "operationId": "getUpload",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadsUploadOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `806` | Invalid upload identifier |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "parameters": [
          {
            "name": "id_upload",
            "in": "path",
            "description": "Upload identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "delete": {
        "tags": [
          "uploads"
        ],
        "summary": "Delete file by identifier",
        "description": "Delete file by identifier",
        "operationId": "deleteUpload",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `806` | Invalid upload identifier |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "parameters": [
          {
            "name": "id_upload",
            "in": "path",
            "description": "Upload identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "forceDelete",
            "in": "query",
            "description": "Delete the file even if a publication still points at it. Without it, a file in use is kept and only unlinked from the library.",
            "required": false,
            "explode": true,
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ]
      },
      "put": {
        "tags": [
          "uploads"
        ],
        "summary": "Update file by identifier",
        "description": "Update file by identifier",
        "operationId": "updateUpload",
        "x-planvortex-identity": [
          "current_user",
          "current_app"
        ],
        "parameters": [
          {
            "name": "id_organization",
            "in": "path",
            "description": "Organization the upload belongs to",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id_upload",
            "in": "path",
            "description": "Upload identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadsUploadOne"
                }
              }
            }
          },
          "400": {
            "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `1101` | Invalid organization |\n| `806` | Invalid upload identifier |\n| `809` | Only videos con have a thumbnail image |\n| `810` | Thumbnail must be an image |\n| `523` | Invalid application |",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled error by the server"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Cover settings of an uploaded file. Only these two properties are read.",
                "properties": {
                  "cover_image": {
                    "type": "string",
                    "description": "Identifier of ANOTHER upload, which must be an image, to use as the cover. Returns error 810 if it does not exist or is not an image. Replacing an existing cover deletes the previous file."
                  },
                  "cover_offset": {
                    "type": "integer",
                    "description": "Point of the video, in milliseconds, used as the cover frame. Used by TikTok videos. WARNING: this property is written unconditionally, so omitting it clears the stored value - always send it back if you want to keep it."
                  }
                }
              },
              "examples": {
                "pickFrame": {
                  "summary": "Use a frame of the video as cover",
                  "value": {
                    "cover_offset": 2500
                  }
                },
                "customImage": {
                  "summary": "Use a previously uploaded image as cover",
                  "value": {
                    "cover_image": "66f1a2b3c4d5e6f7a8b9c0d1",
                    "cover_offset": 0
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "comments": {
      "post": {
        "tags": [
          "webhooks"
        ],
        "summary": "A change happened (notification to your app)",
        "description": "PlanVortex `POST`s to the `webhook_url` of your client app when something happens. **The body is an array of changes**, and each one carries `field` telling you what it is — switch on it and ignore what you do not handle, because the list grows with the product.\n\nTwo shapes travel in that array. A change on an **account** (`WebhookChange`) always carries `id_account`, `id_organization` and `social_network`. A change on an **integration** (`IntegrationWebhookChange`) carries none of those, because an integration hangs off the organization and not off any account.\n\nThe events delivered today are `new_account`, `change_state_account`, `messages`, `messaging_postbacks`, `messaging_seen`, `messaging_error`, `comments` and `integration_error`.\n\n### Comments\n\n`field: \"comments\"` is the one for this section, and the comment itself travels in **`commentObj`** — a property of its own, never in `messageObj`. That is deliberate: a comment is not a message, it has no contact and it hangs off a publication. A consumer that only understands messages sees `commentObj` as absent and ignores the change, which is exactly what should happen.\n\n**Which networks arrive this way:** Facebook and Instagram, because Meta pushes them. YouTube, LinkedIn and Google Business have no comment webhook, so those are polled by a background job and appear in the inbox within a few hours rather than instantly. X is polled too, and only when the organization turns it on, because reading replies there is billed per unit.\n\n**Meta repeats deliveries.** The same comment can arrive more than once; the backend is idempotent about it and so should you be — deduplicate on `commentObj.external_id`.\n\n### Verifying the signature\n\nEvery delivery carries the body signed with your app's secret, twice:\n\n- `x-hub-signature`: `sha1=<hmac>`\n- `x-hub-signature-256`: `sha256=<hmac>`\n\nCompute the HMAC over the **raw** request body — not over a re-serialized copy of the parsed JSON — and compare in constant time. Prefer the sha256 header.\n\nDelivery is best effort: PlanVortex does not retry a webhook that fails.",
        "operationId": "onCommentsWebhook",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "items": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/CommentsWebhookChange"
                    },
                    {
                      "$ref": "#/components/schemas/CommentsIntegrationWebhookChange"
                    }
                  ]
                }
              },
              "example": [
                {
                  "field": "comments",
                  "id_account": "66b0f4a1c2d3e4f5a6b7c8d9",
                  "id_organization": "66b0f4a1c2d3e4f5a6b7c8d0",
                  "social_network": "instagram",
                  "commentObj": {
                    "_id": "66c1a2b3c4d5e6f7a8b9c0d1",
                    "id_account": "66b0f4a1c2d3e4f5a6b7c8d9",
                    "id_organization": "66b0f4a1c2d3e4f5a6b7c8d0",
                    "publication_external_id": "17900000000000000",
                    "external_id": "17800000000000001",
                    "social_network": "instagram",
                    "author": {
                      "external_id": "9988776655",
                      "name": "un_seguidor",
                      "is_own": false
                    },
                    "text": "Me interesa, ¿cuánto cuesta?",
                    "creation_date": "2026-08-19T09:12:00.000Z",
                    "collected_date": "2026-08-19T09:12:01.000Z",
                    "read": false,
                    "replied": false,
                    "hidden": false,
                    "deleted": false
                  }
                }
              ]
            }
          }
        },
        "responses": {
          "200": {
            "description": "Your endpoint acknowledged the delivery. PlanVortex does not retry: a delivery that fails is logged and dropped, so treat the inbox endpoints as the way to catch up on anything you missed."
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Account": {
        "type": "object",
        "description": "A social account connected to an organization.\n\nOn `discord` an account is a **channel**, not a profile: publishing to two channels of the same server costs two accounts of the plan.\n\n`error_code` other than `0` means the connection is broken — an expired token, a permission taken away — and the account has to be connected again.",
        "required": [
          "_id",
          "id_organization",
          "id_client",
          "name",
          "social_network",
          "creation_date",
          "error_code"
        ],
        "properties": {
          "_id": {
            "type": "string"
          },
          "id_organization": {
            "type": "string"
          },
          "id_client": {
            "type": "string",
            "description": "The client the organization hangs from. Denormalized here for the plan checks."
          },
          "name": {
            "type": "string",
            "description": "Display name of the profile, page or channel."
          },
          "username": {
            "type": "string",
            "description": "The handle, when the network has one. Absent on the networks that do not (a Discord channel, a WhatsApp number, a Google Business listing)."
          },
          "social_network": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SocialNetwork"
              }
            ],
            "description": "Network this account belongs to. **Not every network publishes**: `whatsapp` is a messaging channel with no feed, and `google_business` is a local business listing that receives reviews — both can be connected and both appear here, but neither accepts publications (see `GET /social_capabilities`)."
          },
          "creation_date": {
            "type": "string",
            "format": "date-time"
          },
          "error_code": {
            "type": "integer",
            "description": "`0` is a healthy account. Anything else is a PlanVortex error code explaining why the connection stopped working; the account keeps its data but cannot be used until it is reconnected."
          },
          "image": {
            "type": "string",
            "description": "Avatar URL as the network publishes it. Empty string when there is none."
          },
          "followers_count": {
            "type": "integer",
            "description": "Followers the network reports. Absent on an account that has never been measured."
          },
          "next_stats_update": {
            "type": "string",
            "format": "date-time",
            "description": "When the collector will ask the network for this account's stats again."
          },
          "next_comments_update": {
            "type": "string",
            "format": "date-time",
            "description": "When the collector will read this account's comments again. Only on the networks whose comments hang off the account and not off a publication — today, `google_business`, whose reviews belong to the listing."
          },
          "private_message_link": {
            "type": "string",
            "description": "A link that opens a private chat with this account (`m.me`, `ig.me`, `wa.me`). Absent on every other network."
          }
        }
      },
      "AccountsAccountList": {
        "type": "object",
        "properties": {
          "accounts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Account"
            }
          },
          "total": {
            "type": "integer"
          }
        },
        "required": [
          "accounts",
          "total"
        ]
      },
      "AccountsAccountOne": {
        "type": "object",
        "properties": {
          "account": {
            "$ref": "#/components/schemas/Account"
          }
        },
        "required": [
          "account"
        ]
      },
      "AccountsMetricList": {
        "type": "array",
        "items": {
          "type": "string",
          "enum": [
            "page_total_actions",
            "page_call_phone_clicks_logged_in_unique",
            "page_get_directions_clicks_logged_in_unique",
            "page_website_clicks_logged_in_unique",
            "page_post_engagements",
            "page_consumptions_unique",
            "page_negative_feedback",
            "page_negative_feedback_unique",
            "page_fans_online_per_day",
            "page_impressions",
            "page_impressions_unique",
            "page_impressions_paid",
            "page_impressions_paid_unique",
            "page_impressions_organic_v2",
            "page_impressions_organic_unique_v2",
            "page_impressions_viral",
            "page_impressions_viral_unique",
            "page_impressions_nonviral",
            "page_impressions_nonviral_unique",
            "page_posts_impressions",
            "page_posts_impressions_unique",
            "page_posts_impressions_paid",
            "page_posts_impressions_paid_unique",
            "page_posts_impressions_organic",
            "page_posts_impressions_organic_unique",
            "page_posts_served_impressions_organic_unique",
            "page_posts_impressions_viral",
            "page_posts_impressions_viral_unique",
            "page_posts_impressions_nonviral",
            "page_posts_impressions_nonviral_unique",
            "impressions",
            "reach",
            "total_interactions",
            "accounts_engaged",
            "likes",
            "comments",
            "saves",
            "shares",
            "replies",
            "follows_and_unfollows",
            "profile_links_taps",
            "website_clicks",
            "profile_views",
            "paidFollowers",
            "organicFollowerGain",
            "careersPageBannerPromoClicks",
            "careersPagePromoLinksClicks",
            "careersPageEmployeesClicks",
            "careersPageJobsClicks",
            "careersPageViewsUnique",
            "careersPageViews",
            "overviewPageViewsUnique",
            "overviewPageViews",
            "allPageViews",
            "follows",
            "share",
            "views"
          ]
        }
      },
      "AccountsMetricModel": {
        "type": "object",
        "description": "A metric series for one account, already grouped. `group` says what each row covers: a range of 31 days or less is grouped by day, up to 720 by month, and beyond that by year.",
        "required": [
          "stats",
          "group"
        ],
        "properties": {
          "stats": {
            "type": "array",
            "description": "One row per metric and per group. Empty when nothing was measured in the range.",
            "items": {
              "type": "object",
              "required": [
                "date",
                "name",
                "group_value",
                "group",
                "value"
              ],
              "properties": {
                "date": {
                  "type": "string",
                  "format": "date-time",
                  "description": "First measurement of the group, which is what dates the row."
                },
                "name": {
                  "type": "string",
                  "description": "The metric's RAW name, the one the network publishes — the same names `GET .../metric_list` returns."
                },
                "group_value": {
                  "type": "integer",
                  "description": "The group's ordinal: day of the year, month, or year, matching `group`."
                },
                "group": {
                  "type": "string",
                  "enum": [
                    "day",
                    "month",
                    "year"
                  ]
                },
                "value": {
                  "type": "number",
                  "description": "Sum of the measurements in the group."
                }
              }
            }
          },
          "group": {
            "type": "string",
            "enum": [
              "day",
              "month",
              "year"
            ]
          }
        }
      },
      "AccountsPersistentMenu": {
        "type": "array",
        "description": "The chat's fixed menu, one entry per locale, in Meta's own format. The entry with `locale: \"default\"` is required and is what is shown when no other locale matches.",
        "items": {
          "type": "object",
          "properties": {
            "locale": {
              "type": "string",
              "description": "`default`, or a locale such as `es_ES`."
            },
            "composer_input_disabled": {
              "type": "boolean",
              "description": "`true` hides the text box, leaving the menu as the only way to answer."
            },
            "call_to_actions": {
              "type": "array",
              "description": "The buttons. A `postback` sends you its `payload` as a message; a `web_url` opens a page; a `nested` holds more buttons.",
              "items": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        }
      },
      "AccountsSocialAuthorizationMethod": {
        "type": "object",
        "required": [
          "type"
        ],
        "description": "**How** an account of this network is authorized, which is not always \"send the user to this URL\".\n\nNine of the ten networks are `redirect`: open `link` and the network sends the person back to PlanVortex with a code. **WhatsApp is not.** Its sign-up is Meta's *Embedded Signup*: a popup raised by the Facebook JavaScript SDK from your own page, which returns — over `postMessage` — session data (`waba_id`, `phone_number_id`) that no query string carries. Its `link` is therefore an empty string.\n\nBranch on `authorization.type`, never on whether `link` is empty.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "redirect",
              "meta_embedded_signup"
            ],
            "description": "`redirect`: send the user to `link`. `meta_embedded_signup`: open the Meta popup with the fields below."
          },
          "app_id": {
            "type": "string",
            "description": "`meta_embedded_signup` only. Goes to `FB.init({appId})`. It is the same Meta app whose secret PlanVortex uses to exchange the code afterwards."
          },
          "config_id": {
            "type": "string",
            "description": "`meta_embedded_signup` only. Goes to `FB.login(cb, {config_id})` — the Embedded Signup configuration."
          },
          "graph_version": {
            "type": "string",
            "description": "`meta_embedded_signup` only. Goes to `FB.init({version})`. This is the JavaScript SDK version, not the Graph version PlanVortex calls server-side: they move independently."
          },
          "feature_type": {
            "type": "string",
            "description": "`meta_embedded_signup` only. Goes to `extras.featureType`."
          },
          "session_info_version": {
            "type": "string",
            "description": "`meta_embedded_signup` only. Goes to `extras.sessionInfoVersion`."
          }
        }
      },
      "AccountsSocialLinksList": {
        "type": "object",
        "properties": {
          "links": {
            "type": "array",
            "description": "One entry per network that can be connected right now.",
            "items": {
              "type": "object",
              "properties": {
                "social_network": {
                  "$ref": "#/components/schemas/SocialNetwork"
                },
                "link": {
                  "type": "string",
                  "description": "The network's authorization URL. Send the user there. **Empty when `authorization.type` is not `redirect`** — WhatsApp has no URL to give."
                },
                "authorization": {
                  "$ref": "#/components/schemas/AccountsSocialAuthorizationMethod"
                }
              },
              "required": [
                "social_network",
                "link",
                "authorization"
              ]
            }
          }
        },
        "required": [
          "links"
        ]
      },
      "AiContext": {
        "type": "object",
        "description": "Brand context for AI generation. Every field is optional; the ones you fill in are what the model is told about the business.",
        "properties": {
          "brand_name": {
            "type": "string",
            "description": "Commercial name, when it differs from the organization's."
          },
          "description": {
            "type": "string",
            "description": "What the organization does."
          },
          "sector": {
            "type": "string"
          },
          "audience": {
            "type": "string",
            "description": "Who the content is aimed at."
          },
          "value_proposition": {
            "type": "string"
          },
          "website": {
            "type": "string"
          },
          "shop_url": {
            "type": "string"
          },
          "blog_url": {
            "type": "string"
          },
          "social_urls": {
            "type": "array",
            "description": "Social profiles or other reference links.",
            "items": {
              "type": "string"
            }
          },
          "default_tone": {
            "type": "string",
            "description": "Default tone. A tone set on the plan itself wins over this one."
          },
          "keywords": {
            "type": "array",
            "description": "Recurring keywords and hashtags.",
            "items": {
              "type": "string"
            }
          },
          "products": {
            "type": "string",
            "description": "Products or services that may be promoted."
          },
          "avoid": {
            "type": "string",
            "description": "Topics, expressions or competitors that must never appear."
          },
          "notes": {
            "type": "string",
            "description": "Anything else the model should know."
          }
        }
      },
      "AiPlansAiPlan": {
        "type": "object",
        "properties": {
          "_id": {
            "type": "string",
            "example": "66d04a6a427f4c43b9d97f54"
          },
          "id_client": {
            "type": "string"
          },
          "id_organization": {
            "type": "string"
          },
          "keycloak_identifier": {
            "type": "string",
            "description": "User who requested the generation."
          },
          "accounts": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Accounts the plan was generated for."
          },
          "prompt": {
            "type": "string"
          },
          "options": {
            "$ref": "#/components/schemas/AiPlansAiPlanOptions"
          },
          "state": {
            "type": "string",
            "enum": [
              "pending",
              "generating",
              "generated",
              "validated",
              "failed",
              "cancelled"
            ],
            "description": "State machine: pending -> generating -> generated -> validated | failed | cancelled. Poll the plan while state is pending or generating."
          },
          "orchestrator_result": {
            "type": "object",
            "description": "Raw plan returned by the orchestrator, kept for audit (before validation/trimming)."
          },
          "publications": {
            "type": "array",
            "description": "The generated publications — ordinary drafts of the publication domain.\n\n**It is not always the same shape.** Reading one plan (`GET`, `validate`, `retry`) returns whole publications with their files resolved; the LISTING returns their identifiers as strings. Check before using them.",
            "items": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "$ref": "#/components/schemas/Publication"
                }
              ]
            }
          },
          "credits_spent": {
            "type": "integer",
            "description": "AI credits actually consumed by the generation."
          },
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "description": "Last generation error, in the same shape as an API error. Present only in state `failed`.",
            "properties": {
              "code": {
                "type": "integer"
              },
              "message": {
                "type": "string"
              },
              "data": {
                "type": "object",
                "additionalProperties": true
              }
            }
          },
          "attempts": {
            "type": "integer",
            "description": "Generation attempts consumed (transient failures are retried, max 2)."
          },
          "creation_date": {
            "type": "string",
            "format": "date-time"
          },
          "generation_end_date": {
            "type": "string",
            "format": "date-time"
          },
          "organization_context": {
            "allOf": [
              {
                "$ref": "#/components/schemas/AiContext"
              }
            ],
            "description": "SNAPSHOT of the organization's brand context taken when the plan was created, so a retry or a regeneration reproduces the same plan even if the configuration changed. Absent when the plan was asked for without context, or when the organization had none."
          }
        },
        "required": [
          "_id",
          "id_client",
          "id_organization",
          "accounts",
          "prompt",
          "options",
          "state",
          "publications",
          "credits_spent",
          "attempts",
          "creation_date"
        ]
      },
      "AiPlansAiPlanCostEstimate": {
        "type": "object",
        "description": "Deterministic cost estimate computed by the backend (never by the model). BYOK scopes cost 0 credits.",
        "properties": {
          "base_cost": {
            "type": "integer",
            "description": "Mandatory cost (orchestration + target texts). The plan is rejected if this exceeds the available credits."
          },
          "estimated_cost": {
            "type": "integer",
            "description": "Total estimated cost including the financeable images (upper bound)."
          },
          "texts_target": {
            "type": "integer"
          },
          "images_target": {
            "type": "integer"
          },
          "available_credits": {
            "type": "integer"
          }
        },
        "required": [
          "base_cost",
          "estimated_cost",
          "texts_target",
          "images_target",
          "available_credits"
        ]
      },
      "AiPlansAiPlanCreateRequest": {
        "type": "object",
        "required": [
          "prompt",
          "accounts"
        ],
        "properties": {
          "prompt": {
            "type": "string",
            "description": "Theme prompt written by the user."
          },
          "accounts": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Account ids (belonging to the organization) to generate the plan for."
          },
          "options": {
            "$ref": "#/components/schemas/AiPlansAiPlanOptionsInput"
          }
        }
      },
      "AiPlansAiPlanCreateResponse": {
        "type": "object",
        "properties": {
          "ai_plan": {
            "$ref": "#/components/schemas/AiPlansAiPlan"
          },
          "estimated_cost": {
            "type": "integer",
            "description": "Shortcut to estimate.estimated_cost."
          },
          "estimate": {
            "$ref": "#/components/schemas/AiPlansAiPlanCostEstimate"
          }
        },
        "required": [
          "ai_plan",
          "estimated_cost",
          "estimate"
        ]
      },
      "AiPlansAiPlanList": {
        "type": "object",
        "properties": {
          "ai_plans": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AiPlansAiPlan"
            }
          },
          "total": {
            "type": "integer"
          }
        },
        "required": [
          "ai_plans",
          "total"
        ]
      },
      "AiPlansAiPlanOne": {
        "type": "object",
        "properties": {
          "ai_plan": {
            "$ref": "#/components/schemas/AiPlansAiPlan"
          }
        },
        "required": [
          "ai_plan"
        ]
      },
      "AiPlansAiPlanOptions": {
        "type": "object",
        "description": "Generation options as they were STORED, already normalised: `publish_days` comes sorted and deduplicated, and every default has been resolved.",
        "required": [
          "timezone",
          "week_start",
          "publish_days",
          "language",
          "allow_images",
          "shared",
          "use_organization_context",
          "gallery_uploads"
        ],
        "properties": {
          "timezone": {
            "type": "string",
            "default": "Europe/Madrid",
            "description": "IANA timezone for the optimal publish slots (typically the user's browser timezone). publish_date is stored in UTC."
          },
          "week_start": {
            "type": "string",
            "format": "date-time",
            "description": "Start of the week to plan (slots are generated between this date and +7 days). Defaults to now."
          },
          "publish_days": {
            "type": "array",
            "items": {
              "type": "integer",
              "minimum": 1,
              "maximum": 7
            },
            "default": [
              1,
              2,
              3,
              4,
              5,
              6,
              7
            ],
            "description": "Days of the week the plan publishes on, in ISO 8601 numbering (1 = Monday ... 7 = Sunday). Defaults to the whole week. There is still at most ONE publication per day and account, so this is what bounds the size and the cost of the plan: the number of generated posts is (selected days x accounts). Must be a non-empty array of unique integers between 1 and 7, or the request is rejected with 2106. The 7-day window starts at week_start, so each ISO day appears exactly once: with a week_start in mid-week, day 1 (Monday) is the FOLLOWING Monday. If the selected days leave no future slot at all, the request is rejected with 2108."
          },
          "language": {
            "type": "string",
            "default": "es",
            "description": "Language of the generated texts."
          },
          "tone": {
            "type": "string",
            "description": "Optional tone (e.g. 'cercano', 'profesional') passed to the prompt."
          },
          "allow_images": {
            "type": "boolean",
            "default": true,
            "description": "Whether images may be generated. Each image costs 70 AI credits."
          },
          "max_images": {
            "type": "integer",
            "description": "Optional cap on the number of images; the credit budget may reduce it further."
          },
          "gallery_uploads": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Upload ids from the organization's gallery used as visual reference for the generated images."
          },
          "shared": {
            "type": "boolean",
            "default": false,
            "description": "Generate ONE piece of content per day and replicate it across every account, each scheduled at the best hour for ITS network, instead of one publication per account and day. Cheaper — one text and one image per day — and it caps images at 7."
          },
          "use_organization_context": {
            "type": "boolean",
            "default": true,
            "description": "Use the organization's brand context in the prompts. It is copied into the plan as a SNAPSHOT when the plan is created, so a retry or a regeneration uses the context the plan was asked with even if the configuration changed meanwhile."
          }
        }
      },
      "AiPlansAiPlanOptionsInput": {
        "type": "object",
        "description": "Generation options, as you SEND them: every one is optional and the server fills in its default. Optimal publish slots are chosen deterministically by the backend from a fixed table per network, converted to this timezone; the model never invents times.",
        "properties": {
          "timezone": {
            "type": "string",
            "description": "IANA timezone for the optimal publish slots (typically the user's browser timezone). publish_date is stored in UTC. Optional; defaults to `\"Europe/Madrid\"`."
          },
          "week_start": {
            "type": "string",
            "format": "date-time",
            "description": "Start of the week to plan (slots are generated between this date and +7 days). Defaults to now."
          },
          "publish_days": {
            "type": "array",
            "items": {
              "type": "integer",
              "minimum": 1,
              "maximum": 7
            },
            "description": "Days of the week the plan publishes on, in ISO 8601 numbering (1 = Monday ... 7 = Sunday). Defaults to the whole week. There is still at most ONE publication per day and account, so this is what bounds the size and the cost of the plan: the number of generated posts is (selected days x accounts). Must be a non-empty array of unique integers between 1 and 7, or the request is rejected with 2106. The 7-day window starts at week_start, so each ISO day appears exactly once: with a week_start in mid-week, day 1 (Monday) is the FOLLOWING Monday. If the selected days leave no future slot at all, the request is rejected with 2108. Optional; defaults to `[1,2,3,4,5,6,7]`."
          },
          "language": {
            "type": "string",
            "description": "Language of the generated texts. Optional; defaults to `\"es\"`."
          },
          "tone": {
            "type": "string",
            "description": "Optional tone (e.g. 'cercano', 'profesional') passed to the prompt."
          },
          "allow_images": {
            "type": "boolean",
            "description": "Whether images may be generated. Each image costs 70 AI credits. Optional; defaults to `true`."
          },
          "max_images": {
            "type": "integer",
            "description": "Optional cap on the number of images; the credit budget may reduce it further."
          },
          "gallery_uploads": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Upload ids from the organization's gallery used as visual reference for the generated images."
          },
          "shared": {
            "type": "boolean",
            "description": "Generate ONE piece of content per day and replicate it across every account, each scheduled at the best hour for ITS network, instead of one publication per account and day. Cheaper — one text and one image per day — and it caps images at 7. Optional; defaults to `false`."
          },
          "use_organization_context": {
            "type": "boolean",
            "description": "Use the organization's brand context in the prompts. It is copied into the plan as a SNAPSHOT when the plan is created, so a retry or a regeneration uses the context the plan was asked with even if the configuration changed meanwhile. Optional; defaults to `true`."
          }
        }
      },
      "AppsClientApp": {
        "type": "object",
        "properties": {
          "_id": {
            "type": "string"
          },
          "id_client": {
            "type": "string"
          },
          "id_user": {
            "type": "string",
            "description": "The user who created the app, when it was created from the panel."
          },
          "name": {
            "type": "string"
          },
          "keycloak_client_idenfifier": {
            "type": "string",
            "description": "The app's `client_id`."
          },
          "allowed_domains": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "redirect_urls": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "webhook_url": {
            "type": "string",
            "description": "Where PlanVortex posts events, when one is configured.\n\n**The body is an array of changes, not a single object**, and it carries two signature headers computed with this app's secret over the **raw** body: `x-hub-signature` (`sha1=<hex>`) and `x-hub-signature-256` (`sha256=<hex>`). Verify against the bytes you received — parsing the JSON and re-serialising it changes them and the signature will not match.\n\nThe events delivered today are `new_account`, `change_state_account`, `messages`, `messaging_postbacks`, `messaging_seen`, `messaging_error`, `comments` and `integration_error`. The payload is documented in the `comments` specification. Delivery is best effort: PlanVortex does not retry a webhook that fails."
          },
          "creation_date": {
            "type": "string",
            "format": "date-time"
          },
          "deleted": {
            "type": "boolean",
            "description": "Deleting an app marks it instead of removing it — the Keycloak client is gone, so it can no longer get a token — and the listing filters those out."
          },
          "deleted_date": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "_id",
          "id_client",
          "name",
          "keycloak_client_idenfifier",
          "allowed_domains",
          "redirect_urls",
          "creation_date",
          "deleted"
        ],
        "description": "An app: the credentials a third-party integration authenticates with.\n\n**The secret is not here.** It lives in Keycloak and is read with `GET /clients/{id_client}/apps/{id_app}/secret`, which needs a user token."
      },
      "AppsClientAppInput": {
        "type": "object",
        "required": [
          "name",
          "keycloak_client_idenfifier"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "A name for the app. Cannot be blank (error 525)."
          },
          "keycloak_client_idenfifier": {
            "type": "string",
            "description": "The app's `client_id`, which is what you send to `POST /oauth/token`. It has to be unique across PlanVortex (error 534). The spelling of the field is historical and kept for compatibility."
          },
          "allowed_domains": {
            "type": "array",
            "description": "Origins allowed to call the API with this app's identity. Every entry has to be a valid URL (error 531).",
            "items": {
              "type": "string"
            }
          },
          "redirect_urls": {
            "type": "array",
            "description": "URLs the connect flow is allowed to come back to. A `redirect_uri` that is not in this list is refused when issuing a temporal connect token (error 532).",
            "items": {
              "type": "string"
            }
          },
          "webhook_url": {
            "type": "string",
            "description": "Where PlanVortex posts events. Has to be a valid URL (error 535). See `ClientApp.webhook_url` for what arrives and how it is signed."
          }
        }
      },
      "AppsOAuthError": {
        "type": "object",
        "description": "The error shape of the token endpoint, and **only** of the token endpoint. Every other endpoint in the API answers with `Error` instead.",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "invalid_request",
              "invalid_client",
              "unsupported_grant_type",
              "slow_down",
              "server_error"
            ]
          },
          "error_description": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "AppsTokenRequest": {
        "type": "object",
        "required": [
          "grant_type"
        ],
        "properties": {
          "grant_type": {
            "type": "string",
            "description": "Only `client_credentials` is supported.",
            "enum": [
              "client_credentials"
            ]
          },
          "client_id": {
            "type": "string",
            "description": "The app's identifier. Can travel here or in `Authorization: Basic`."
          },
          "client_secret": {
            "type": "string",
            "description": "The app's secret. Can travel here or in `Authorization: Basic`."
          },
          "scope": {
            "type": "string",
            "description": "Optional. Passed through to the identity provider; there are no PlanVortex-defined scopes today."
          }
        }
      },
      "AppsTokenResponse": {
        "type": "object",
        "properties": {
          "access_token": {
            "type": "string",
            "description": "Send it as `Authorization: Bearer <access_token>`."
          },
          "token_type": {
            "type": "string",
            "description": "Always `Bearer`."
          },
          "expires_in": {
            "type": "integer",
            "description": "Seconds the token is valid for. Refresh shortly before it runs out; there is no refresh token."
          },
          "scope": {
            "type": "string"
          }
        },
        "required": [
          "access_token",
          "token_type",
          "expires_in"
        ]
      },
      "CatalogAspectRatios": {
        "type": "object",
        "description": "Accepted crops. `values` and `text` are parallel arrays: same index, same ratio.",
        "properties": {
          "values": {
            "type": "array",
            "description": "The ratio as a number (width divided by height), which is what validation compares against.",
            "items": {
              "type": "number"
            }
          },
          "text": {
            "type": "array",
            "description": "The same ratio written the way a person reads it.",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "values",
          "text"
        ]
      },
      "CatalogSocialLimits": {
        "type": "object",
        "properties": {
          "characters": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CatalogSocialLimitsMap"
              }
            ],
            "description": "Maximum length of a publication's text. Bluesky counts graphemes, everyone else counts characters."
          },
          "max_post_bytes": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CatalogSocialLimitsMap"
              }
            ],
            "description": "Second text limit, in UTF-8 bytes. `0` means the network does not measure text in bytes; only Bluesky does, at 3.000."
          },
          "comment_characters": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CatalogSocialLimitsMap"
              }
            ],
            "description": "Maximum length of a reply to a comment. `0` means the network has no comments."
          },
          "title_characters": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CatalogSocialLimitsMap"
              }
            ],
            "description": "Maximum length of the title. `0` means the network has no title field at all."
          },
          "total_images": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CatalogSocialLimitsMap"
              }
            ],
            "description": "How many images one publication accepts. `0` means images are not a publication on that network."
          },
          "video_duration_in_seconds": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CatalogSocialLimitsMap"
              }
            ],
            "description": "Maximum video duration. A network that limits weight instead of duration is not here but in `max_file_size_mb`."
          },
          "max_file_size_mb": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CatalogSocialLimitsMap"
              }
            ],
            "description": "Maximum size of one file, in megabytes."
          }
        },
        "required": [
          "characters",
          "max_post_bytes",
          "comment_characters",
          "title_characters",
          "total_images",
          "video_duration_in_seconds",
          "max_file_size_mb"
        ]
      },
      "CatalogSocialLimitsMap": {
        "type": "object",
        "description": "One number per network. Every network in `/social_networks` is present.",
        "additionalProperties": {
          "type": "integer"
        }
      },
      "ClientsAiScopeSetting": {
        "type": "object",
        "description": "AI provider configuration for a single scope (BYOK). On write, api_key is required the first time and is stored encrypted; on subsequent writes it can be omitted to keep the existing key (send only provider/model). On read, the api_key is NEVER returned — only has_api_key is exposed.",
        "properties": {
          "provider": {
            "type": "string",
            "enum": [
              "openrouter",
              "openai",
              "google",
              "anthropic"
            ],
            "description": "Fixed catalogue of allowed providers. The provider must support the capability of the scope (text for orchestrator/text, image for image)."
          },
          "model": {
            "type": "string",
            "description": "Model identifier at the chosen provider (e.g. google/gemini-3-flash)."
          },
          "api_key": {
            "type": "string",
            "writeOnly": true,
            "description": "Provider API key. Write-only: accepted on PUT, stored encrypted (AES-256-GCM), never returned."
          },
          "has_api_key": {
            "type": "boolean",
            "readOnly": true,
            "description": "Read-only. True when an encrypted API key is stored for this scope."
          }
        },
        "required": [
          "provider",
          "model"
        ]
      },
      "ClientsAiSettings": {
        "type": "object",
        "description": "Per-scope AI provider configuration (BYOK). Only the scopes you send are touched; a scope set to `null` clears its configuration and returns that scope to PlanVortex credits. `orchestrator` and `text` need a text-capable provider, `image` needs an image-capable one. `video` is reserved for a later phase.",
        "properties": {
          "orchestrator": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ClientsAiScopeSetting"
              },
              {
                "type": "null"
              }
            ],
            "description": "Configuration of the `orchestrator` scope. **`null` clears it** and returns the scope to PlanVortex credits."
          },
          "text": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ClientsAiScopeSetting"
              },
              {
                "type": "null"
              }
            ],
            "description": "Configuration of the `text` scope. **`null` clears it** and returns the scope to PlanVortex credits."
          },
          "image": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ClientsAiScopeSetting"
              },
              {
                "type": "null"
              }
            ],
            "description": "Configuration of the `image` scope. **`null` clears it** and returns the scope to PlanVortex credits."
          }
        }
      },
      "ClientsClient": {
        "type": "object",
        "description": "Who contracts the plan and whom the organizations hang from.",
        "required": [
          "_id",
          "name",
          "actual_plan",
          "creation_date"
        ],
        "properties": {
          "_id": {
            "type": "string",
            "example": "66d04a6a427f4c43b9d97f54"
          },
          "name": {
            "type": "string"
          },
          "client_type": {
            "type": "string",
            "enum": [
              "personal",
              "company"
            ]
          },
          "creation_date": {
            "type": "string",
            "format": "date-time"
          },
          "trial_tested": {
            "type": "boolean",
            "description": "Whether this client has already used its trial."
          },
          "actual_plan": {
            "$ref": "#/components/schemas/ClientsPlan"
          },
          "ai_settings": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ClientsAiSettings"
              }
            ],
            "description": "Read-only view of the client's own AI provider configuration (BYOK). Written via `PUT /clients/{id_client}/ai-settings`. API keys are stored encrypted and are NEVER returned: each scope only exposes provider, model and has_api_key."
          },
          "actual_use": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PlanData"
              }
            ],
            "description": "Current consumption across every organization of the client. **Only present with `getUse=true`.** `twitter_credits` and `ai_credits` are what has been spent this calendar month."
          },
          "actual_asigned": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PlanData"
              }
            ],
            "description": "What is already handed out to the client's root organizations, which is what is left to assign. **Only present with `getUse=true`.**"
          }
        }
      },
      "ClientsClientList": {
        "type": "object",
        "properties": {
          "clients": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ClientsClient"
            }
          },
          "total": {
            "type": "integer"
          }
        },
        "required": [
          "clients",
          "total"
        ]
      },
      "ClientsClientOne": {
        "type": "object",
        "properties": {
          "client": {
            "$ref": "#/components/schemas/ClientsClient"
          }
        },
        "required": [
          "client"
        ]
      },
      "ClientsOrganizationCreate": {
        "type": "object",
        "description": "Body accepted when creating an organization. Only these properties are read.",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Organization name."
          },
          "actual_plan": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PlanData"
              }
            ],
            "description": "Resources assigned to this organization, taken from what the client has contracted."
          }
        }
      },
      "ClientsOrganizationList": {
        "type": "object",
        "properties": {
          "organizations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Organization"
            }
          },
          "total": {
            "type": "integer"
          }
        },
        "required": [
          "organizations",
          "total"
        ]
      },
      "ClientsOrganizationOne": {
        "type": "object",
        "properties": {
          "organization": {
            "$ref": "#/components/schemas/Organization"
          }
        },
        "required": [
          "organization"
        ]
      },
      "ClientsOrganizationUpdate": {
        "type": "object",
        "description": "Body accepted when updating an organization. NOTE: `name` and `parent_organization` are NOT updatable through this endpoint - the server keeps their current value and silently ignores them.",
        "properties": {
          "actual_plan": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PlanData"
              }
            ],
            "description": "New resource assignment. Validated against what is left of the client's contracted plan; lowering it below what the organization already uses is rejected."
          },
          "stats_settings": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StatsSettings"
              }
            ],
            "description": "Statistics collection settings."
          }
        }
      },
      "ClientsPlan": {
        "type": "object",
        "description": "The client's SUBSCRIPTION, which is not the same thing as the resources it grants: the numbers live in `plan_data`.",
        "required": [
          "enabled",
          "status",
          "plan_identifier",
          "plan_data",
          "isEnabled"
        ],
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "Whether the plan is usable. A disabled client cannot connect accounts or publish."
          },
          "status": {
            "type": "string",
            "description": "The subscription's state at the payment provider (`active`, `past_due`, `canceled`, `trialing`, `unpaid`...). A cancelled subscription still works until `current_period_end`."
          },
          "isEnabled": {
            "type": "boolean",
            "description": "The same thing as `enabled`, recomputed on read rather than stored. Read either; they only disagree if the plan was never saved after changing."
          },
          "require_action": {
            "type": "boolean",
            "description": "The payment needs the customer to do something (3-D Secure, a new card)."
          },
          "plan_identifier": {
            "type": "string",
            "enum": [
              "free",
              "basic",
              "pro",
              "custom"
            ],
            "description": "Which plan was contracted. `custom` carries its own numbers in `plan_data`."
          },
          "plan_data": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PlanData"
              }
            ],
            "description": "**The client's actual limits**, resolved: the named plan's table, or the agreed numbers when `plan_identifier` is `custom`. This is what to read, not the plan's name."
          },
          "current_period_start": {
            "type": "string",
            "format": "date-time"
          },
          "current_period_end": {
            "type": "string",
            "format": "date-time",
            "description": "When the current billing period ends. A cancelled plan keeps working until then."
          },
          "stripe_customer_id": {
            "type": "string"
          }
        }
      },
      "ClientsRolesClientPermissions": {
        "type": "string",
        "enum": [
          "client:update",
          "client:read",
          "client:delete",
          "client_app:create",
          "client_app:update",
          "client_app:read",
          "client_app:delete",
          "client_roles:create",
          "client_roles:update",
          "client_roles:read",
          "client_roles:delete",
          "client_roles_users:read",
          "client_roles_users:create",
          "client_roles_users:delete",
          "client_organization:create",
          "client_organization:update",
          "client_organization:read",
          "client_organization:delete",
          "client_organization_roles:read",
          "client_organization_roles:create",
          "client_organization_roles:update",
          "client_organization_roles:delete",
          "client_organization_users_roles:read",
          "client_organization_users_roles:create",
          "client_organization_users_roles:delete",
          "client_organization_accounts:read",
          "client_organization_accounts:create",
          "client_organization_accounts:update",
          "client_organization_accounts:delete",
          "client_organization_files:read",
          "client_organization_files:create",
          "client_organization_files:update",
          "client_organization_files:delete",
          "client_organization_publications:read",
          "client_organization_publications:create",
          "client_organization_publications:update",
          "client_organization_publications:delete",
          "client_organization_publications_stats:read",
          "client_organization_account_stats:read"
        ]
      },
      "ClientsRolesClientPermissionsList": {
        "type": "array",
        "items": {
          "$ref": "#/components/schemas/ClientsRolesClientPermissions"
        }
      },
      "ClientsRolesClientPermissionsOne": {
        "type": "object",
        "properties": {
          "permissions": {
            "$ref": "#/components/schemas/ClientsRolesClientPermissionsList"
          }
        }
      },
      "ClientsRolesRole": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "permissions": {
            "$ref": "#/components/schemas/ClientsRolesClientPermissionsList"
          }
        }
      },
      "ClientsRolesRoleInput": {
        "type": "object",
        "description": "Name and permission list of a role.",
        "required": [
          "name",
          "permissions"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Role name, shown when inviting a user."
          },
          "permissions": {
            "type": "array",
            "description": "Permissions granted by this role.",
            "items": {
              "type": "string",
              "enum": [
                "client:update",
                "client:read",
                "client:delete",
                "client_app:create",
                "client_app:update",
                "client_app:read",
                "client_app:delete",
                "client_roles:create",
                "client_roles:update",
                "client_roles:read",
                "client_roles:delete",
                "client_roles_users:read",
                "client_roles_users:create",
                "client_roles_users:delete",
                "client_organization:create",
                "client_organization:update",
                "client_organization:read",
                "client_organization:delete",
                "client_organization_roles:read",
                "client_organization_roles:create",
                "client_organization_roles:update",
                "client_organization_roles:delete",
                "client_organization_users_roles:read",
                "client_organization_users_roles:create",
                "client_organization_users_roles:delete",
                "client_organization_accounts:read",
                "client_organization_accounts:create",
                "client_organization_accounts:update",
                "client_organization_accounts:delete",
                "client_organization_products:create",
                "client_organization_products:update",
                "client_organization_products:read",
                "client_organization_products:delete",
                "client_organization_files:read",
                "client_organization_files:create",
                "client_organization_files:update",
                "client_organization_files:delete",
                "client_organization_publications:read",
                "client_organization_publications:create",
                "client_organization_publications:update",
                "client_organization_publications:delete",
                "client_organization_publications_stats:read",
                "client_organization_account_stats:read",
                "client_organization_messages:create",
                "client_organization_messages:read",
                "client_organization_messages:delete",
                "client_organization_contacts:create",
                "client_organization_contacts:read",
                "client_organization_contacts:update",
                "client_organization_contacts:delete",
                "client_organization_ai_plans:create",
                "client_organization_ai_plans:read",
                "client_organization_ai_plans:update",
                "client_organization_ai_plans:delete"
              ]
            }
          }
        }
      },
      "ClientsRolesRoleList": {
        "type": "object",
        "properties": {
          "roles": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ClientsRolesRole"
            }
          },
          "total": {
            "type": "integer"
          }
        }
      },
      "ClientsRolesRoleOne": {
        "type": "object",
        "properties": {
          "rol": {
            "$ref": "#/components/schemas/ClientsRolesRole"
          }
        }
      },
      "ClientsRolesUser": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "username": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "firstname": {
            "type": "string"
          },
          "lastname": {
            "type": "string"
          }
        }
      },
      "ClientsRolesUserList": {
        "type": "object",
        "properties": {
          "users": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ClientsRolesUser"
            }
          },
          "total": {
            "type": "integer"
          }
        }
      },
      "CommentsComment": {
        "type": "object",
        "description": "A comment — or a review — as PlanVortex stores it. Remember it is a **snapshot** of what the network said at `collected_date`; the live thread endpoints return the same shape reconciled against the network.",
        "properties": {
          "_id": {
            "type": "string"
          },
          "id_account": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "$ref": "#/components/schemas/Account"
              }
            ],
            "description": "The connected account it arrived on.\n\n**It is not always the same shape.** The inbox listing (`GET /organizations/{id}/comments`) returns the whole account resolved; every other operation — the live threads, the reply, the update — returns its identifier as a string. Check before using it."
          },
          "id_organization": {
            "type": "string"
          },
          "id_publication": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "$ref": "#/components/schemas/Publication"
              }
            ],
            "description": "**Your** publication, when there is one — and there often is not: a video uploaded to the channel by hand, a post that predates PlanVortex, and every Google Business review have comments with no publication of ours behind them. What always identifies the target is `publication_external_id`.\n\nSame asymmetry as `id_account`: the inbox listing resolves it into the whole publication, every other operation returns the identifier as a string."
          },
          "publication_external_id": {
            "type": "string",
            "description": "What the comment hangs off, on the network: the post/video id in five networks, and the **listing** (`locations/{id}`) for a Google Business review."
          },
          "external_id": {
            "type": "string",
            "description": "The comment's id on the network. Unique per account, and what makes repeated webhook deliveries idempotent.\n\nOne exception worth knowing: a Google Business reply has no id of its own — it is a *field* of the review — so PlanVortex fabricates a stable one, `{reviewId}/reply`."
          },
          "parent_external_id": {
            "type": "string",
            "description": "Present only when this is a reply to another comment"
          },
          "social_network": {
            "$ref": "#/components/schemas/CommentsCommentNetworkName"
          },
          "author": {
            "$ref": "#/components/schemas/CommentsCommentAuthor"
          },
          "text": {
            "type": "string",
            "description": "**May legitimately be empty.** A stars-only review carries no text at all, so render `rating` alongside it and never assume a blank comment is a loading failure."
          },
          "rating": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5,
            "description": "Star rating of a **review**. Present only on review networks — today Google Business. Its absence means \"this network has no such thing\", never zero.\n\nIt is not decorative: without it a one-star review and a five-star one are the same row, and a stars-only review is a blank line."
          },
          "creation_date": {
            "type": "string",
            "format": "date-time",
            "description": "When it was written **on the network**. This is what orders the inbox."
          },
          "collected_date": {
            "type": "string",
            "format": "date-time",
            "description": "When PlanVortex last read it. The inbox is a photograph and this says how old it is."
          },
          "read": {
            "type": "boolean",
            "description": "Yours, not the network's. A live read never overwrites it."
          },
          "replied": {
            "type": "boolean",
            "description": "Yours, not the network's. Set when you reply through the API."
          },
          "hidden": {
            "type": "boolean",
            "description": "Hidden on the network. A hidden comment is never swept as deleted: on YouTube, hiding one makes the API stop returning it forever, and without that exception hiding would be indistinguishable from deleting."
          },
          "deleted": {
            "type": "boolean",
            "description": "Gone from the network. The row is kept so it is not created again by the next read or by a repeated webhook; it stops appearing in the inbox."
          },
          "like_count": {
            "type": "integer",
            "description": "Absent when the network does not publish it — not zero"
          },
          "reply_count": {
            "type": "integer",
            "description": "Absent when the network does not publish it — not zero"
          },
          "our_reply_external_id": {
            "type": "string",
            "description": "The network id of your reply, so you can find it or delete it later"
          }
        },
        "required": [
          "_id",
          "id_account",
          "id_organization",
          "publication_external_id",
          "external_id",
          "social_network",
          "author",
          "text",
          "creation_date",
          "collected_date",
          "read",
          "replied",
          "hidden",
          "deleted"
        ]
      },
      "CommentsCommentActions": {
        "type": "object",
        "description": "What one network lets you do to a comment. Four booleans and never fewer: an absent key would be indistinguishable from an oversight.",
        "properties": {
          "reply": {
            "type": "boolean"
          },
          "hide": {
            "type": "boolean"
          },
          "delete_own": {
            "type": "boolean",
            "description": "Delete a comment **you** wrote. On Google Business this means your reply to a review — the review itself is never deletable."
          },
          "delete_others": {
            "type": "boolean",
            "description": "Delete somebody else's comment"
          }
        },
        "required": [
          "reply",
          "hide",
          "delete_own",
          "delete_others"
        ]
      },
      "CommentsCommentAuthor": {
        "type": "object",
        "description": "Who wrote it. Embedded in the comment and **not** a contact: a YouTube commenter has no private inbox you could ever write to, so they get no contact record.",
        "properties": {
          "external_id": {
            "type": "string",
            "description": "The author's id on the network. When the network gives none — Google Business publishes no identifier for a reviewer — this falls back to the review's own id, which means two reviews by the same person look like two different authors. There is no way around it from the API."
          },
          "name": {
            "type": "string",
            "description": "Display name. Always present for a review, including anonymous ones, which get a placeholder rather than an empty string."
          },
          "profile_pic": {
            "type": "string"
          },
          "is_own": {
            "type": "boolean",
            "description": "Whether the connected account wrote it. This is what separates \"delete mine\" from \"delete theirs\", and what keeps your own replies out of the inbox."
          }
        },
        "required": [
          "external_id",
          "is_own"
        ]
      },
      "CommentsCommentNetworkName": {
        "type": "string",
        "description": "A network that has comments. Treat it as an open list: a new one is added before your integration hears about it.",
        "enum": [
          "facebook",
          "instagram",
          "twitter",
          "linkedin",
          "youtube",
          "google_business",
          "bluesky",
          "discord"
        ]
      },
      "CommentsCommentThread": {
        "type": "object",
        "description": "A live read: asked of the network and reconciled with what was stored.",
        "properties": {
          "comments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CommentsComment"
            }
          },
          "total": {
            "type": "integer",
            "description": "What the network says the total is. On a Google Business listing it is the number of **reviews**, which is not the length of `comments`: your replies travel in the same array as children of the review they answer."
          },
          "next_cursor": {
            "type": "string",
            "description": "Opaque page token from the network. Pass it back as `offset`; absent means there is no next page."
          },
          "credits_consumed": {
            "type": "integer",
            "description": "X credits this read spent from the client's monthly pool. `0` on every other network. Charged after the fact and by real units, so a failed read charges nothing."
          }
        },
        "required": [
          "comments",
          "total",
          "credits_consumed"
        ]
      },
      "CommentsIntegrationWebhookChange": {
        "type": "object",
        "description": "One change in the array PlanVortex posts to your app's `webhook_url`, when an **integration** stopped working: a revoked Google Drive token, a feed that no longer answers, a publication quota that ran out.\n\nIt carries neither `id_account` nor `social_network`, because an integration hangs off the organization and not off any account — which is exactly why it is a type of its own. A consumer that only understands account changes sees a `field` it does not know and ignores it, which is what should happen.",
        "properties": {
          "field": {
            "type": "string",
            "enum": [
              "integration_error"
            ]
          },
          "id_integration": {
            "type": "string"
          },
          "id_organization": {
            "type": "string"
          },
          "provider": {
            "type": "string",
            "description": "`google_drive` or `rss` today. **This list grows**: treat it as an open enumeration."
          },
          "error_code": {
            "type": "integer",
            "description": "PlanVortex error code saying what went wrong. Integration codes live in the 2200-2299 range."
          }
        },
        "required": [
          "field",
          "id_integration",
          "id_organization",
          "provider",
          "error_code"
        ]
      },
      "CommentsSocialCapabilities": {
        "type": "object",
        "description": "The coarse gates of one network",
        "properties": {
          "publications": {
            "type": "boolean"
          },
          "messages": {
            "type": "boolean"
          },
          "products": {
            "type": "boolean"
          },
          "webhooks": {
            "type": "boolean"
          },
          "persistent_menu": {
            "type": "boolean"
          },
          "comments": {
            "type": "boolean"
          }
        },
        "required": [
          "publications",
          "messages",
          "products",
          "webhooks",
          "persistent_menu",
          "comments"
        ]
      },
      "CommentsWebhookChange": {
        "type": "object",
        "description": "One change in the array PlanVortex posts to your app's `webhook_url`, when the change concerns a social **account**.\n\nAn integration that stopped working has a shape of its own — `IntegrationWebhookChange` — and one delivery can mix both. Switch on `field`, and ignore what you do not handle.",
        "properties": {
          "field": {
            "type": "string",
            "description": "What kind of change this is. **Treat it as an open list** and ignore what you do not handle: it grows with the product.\n\n- `new_account` / `change_state_account`: an account was connected, or its state changed — it stopped working, its token was refreshed, it was disconnected.\n- `messages`: a message came in. It travels in `messageObj`.\n- `messaging_postbacks`: the contact pressed a button or a quick reply. Also in `messageObj`.\n- `messaging_seen`: the contact read the conversation. `messageObj` carries the message they read, when we still have it.\n- `messaging_error`: the network refused a message we sent. The reason is in `messageObj.message_errors`.\n- `comments`: a comment came in. It travels in `commentObj`, never in `messageObj`.",
            "enum": [
              "new_account",
              "change_state_account",
              "messages",
              "messaging_postbacks",
              "messaging_seen",
              "messaging_error",
              "comments"
            ]
          },
          "id_account": {
            "type": "string"
          },
          "id_organization": {
            "type": "string"
          },
          "social_network": {
            "$ref": "#/components/schemas/SocialNetwork"
          },
          "commentObj": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CommentsComment"
              }
            ],
            "description": "The comment. Present only when `field` is `comments`, and absent even then if the author deleted a comment we had never seen."
          },
          "messageObj": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Message"
              }
            ],
            "description": "The message. Present for the messaging fields, never for a comment — a comment is not a message and does not travel in here.\n\nIt arrives **populated**: `contact_id`, `from_contact_id` and `message_options.files` carry whole objects. On `messaging_seen` and `messaging_error` it can be absent, because the message being acknowledged may not be one of ours."
          },
          "id_contact": {
            "type": "string",
            "description": "Only for messaging fields. A comment has no contact: its author is not someone you can write to."
          },
          "originalChange": {
            "type": "object",
            "additionalProperties": true,
            "description": "The raw payload the social network sent, passed through untouched. Absent on the changes PlanVortex raises itself, such as `new_account`."
          }
        },
        "required": [
          "field",
          "id_account",
          "id_organization",
          "social_network"
        ]
      },
      "Contact": {
        "type": "object",
        "description": "A person the organization exchanges messages with. The same contact can be reachable on several channels — that is what `social_identifiers` is.",
        "properties": {
          "_id": {
            "type": "string"
          },
          "id_organization": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "profile_image": {
            "type": "string",
            "description": "URL of the contact's avatar on the network. It belongs to the network and it can stop working."
          },
          "social_identifiers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SocialIdentifier"
            }
          },
          "extra_data": {
            "$ref": "#/components/schemas/ContactExtraData"
          },
          "creator_keycloak_identifier": {
            "type": "string",
            "description": "The user who created the contact, when it was created from the panel instead of arriving from a network."
          },
          "creation_date": {
            "type": "string",
            "format": "date-time"
          },
          "last_contact_update": {
            "type": "string",
            "format": "date-time",
            "description": "Last time the contact's profile was refreshed from the network."
          }
        },
        "required": [
          "_id",
          "id_organization",
          "social_identifiers",
          "creation_date",
          "last_contact_update"
        ]
      },
      "ContactChannel": {
        "type": "string",
        "description": "Where a contact can be reached. Every social network that has messaging, plus `email` for a contact created by hand instead of arriving from a network.\n\n**This list grows** with the networks: treat it as an open enumeration.",
        "enum": [
          "facebook",
          "instagram",
          "linkedin",
          "tiktok",
          "twitter",
          "whatsapp",
          "youtube",
          "google_business",
          "bluesky",
          "discord",
          "email"
        ]
      },
      "ContactExtraData": {
        "type": "object",
        "description": "Your own fields on the contact. The address block is known to PlanVortex; the six generic properties are yours to use, and they are what `extra_data` filters on.",
        "properties": {
          "place_id": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "country_code": {
            "type": "string"
          },
          "zip_code": {
            "type": [
              "string",
              "number"
            ]
          },
          "number": {
            "type": [
              "string",
              "number"
            ]
          },
          "building": {
            "type": [
              "string",
              "number"
            ]
          },
          "floor": {
            "type": [
              "string",
              "number"
            ]
          },
          "door": {
            "type": [
              "string",
              "number"
            ]
          },
          "coords": {
            "type": "array",
            "description": "Longitude and latitude, in that order.",
            "items": {
              "type": "number"
            }
          },
          "string_property": {
            "type": "string"
          },
          "string_property2": {
            "type": "string"
          },
          "boolean_property": {
            "type": "boolean"
          },
          "boolean_property2": {
            "type": "boolean"
          },
          "number_property": {
            "type": "number"
          },
          "number_property2": {
            "type": "number"
          }
        }
      },
      "ContactsContactCreate": {
        "type": "object",
        "required": [
          "social_identifiers"
        ],
        "description": "A new contact. **At least one identifier is mandatory** (`ERROR_CODE_1601` otherwise): a contact with no channel is a contact nobody can write to.\n\nCreating is idempotent on the FIRST identifier: if the organization already has a contact with that channel and that `external_identifier`, you get the existing one back untouched — `name`, `profile_image` and `extra_data` of the request are ignored. There is no \"already exists\" error.",
        "properties": {
          "name": {
            "type": "string"
          },
          "profile_image": {
            "type": "string"
          },
          "social_identifiers": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/ContactsSocialIdentifierInput"
            }
          },
          "extra_data": {
            "$ref": "#/components/schemas/ContactExtraData"
          }
        }
      },
      "ContactsContactUpdate": {
        "type": "object",
        "description": "Changes to a contact. `name`, `profile_image` and `social_identifiers` are left alone when you omit them.\n\n**`extra_data` is the exception and it is destructive**: it is written with whatever the body carries, so omitting it ERASES every custom field on the contact. Read the contact, change what you need and send the whole block back.",
        "properties": {
          "name": {
            "type": "string"
          },
          "profile_image": {
            "type": "string"
          },
          "social_identifiers": {
            "type": "array",
            "description": "Replaces the whole list, it does not merge into it. Omit it to keep the current one.",
            "items": {
              "$ref": "#/components/schemas/ContactsSocialIdentifierInput"
            }
          },
          "extra_data": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ContactExtraData"
              }
            ],
            "description": "Written as sent. **Omitting it erases the contact's custom fields.**"
          }
        }
      },
      "ContactsSocialIdentifierInput": {
        "type": "object",
        "required": [
          "social_network"
        ],
        "description": "The same person on one channel, as you SEND it. `_id` is optional here and PlanVortex mints one when it is missing — which is the difference with `SocialIdentifier`, where it always travels back.",
        "properties": {
          "_id": {
            "type": "string",
            "description": "Only when you are keeping an identifier the API already gave you."
          },
          "external_identifier": {
            "type": "string",
            "description": "The contact id on that channel — the phone number on WhatsApp, the PSID on Messenger. Optional in the model, but a contact without it cannot be written to, and it is what deduplicates on create."
          },
          "social_network": {
            "$ref": "#/components/schemas/ContactChannel"
          }
        }
      },
      "DashboardAccountWithError": {
        "type": "object",
        "description": "A connected account that has stopped working: an expired token or a revoked permission. Until it is reconnected it neither publishes nor measures.",
        "properties": {
          "_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "username": {
            "type": "string"
          },
          "social_network": {
            "$ref": "#/components/schemas/SocialNetwork"
          },
          "image": {
            "type": "string"
          },
          "error_code": {
            "type": "integer",
            "description": "PlanVortex error code that broke it. `0` means healthy, so anything here is non-zero."
          }
        },
        "required": [
          "_id",
          "name",
          "social_network",
          "error_code"
        ]
      },
      "DashboardDashboard": {
        "type": "object",
        "properties": {
          "range": {
            "$ref": "#/components/schemas/DashboardDashboardRange"
          },
          "available_blocks": {
            "type": "object",
            "description": "Which blocks the caller was allowed to see. A `false` here is a permission (or plan) answer; a block that is `true` but empty means there is no data.",
            "properties": {
              "health": {
                "type": "boolean"
              },
              "publications": {
                "type": "boolean"
              },
              "publication_metrics": {
                "type": "boolean"
              },
              "account_metrics": {
                "type": "boolean"
              },
              "plan_use": {
                "type": "boolean"
              },
              "ai_plans": {
                "type": "boolean"
              },
              "messages": {
                "type": "boolean"
              }
            },
            "required": [
              "health",
              "publications",
              "publication_metrics",
              "account_metrics",
              "plan_use",
              "ai_plans",
              "messages"
            ]
          },
          "health": {
            "type": "object",
            "description": "What needs fixing today. Each half has its own permission: somebody who cannot read accounts still sees the failed publications.",
            "properties": {
              "accounts_with_errors": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/DashboardAccountWithError"
                }
              },
              "publications_with_errors": {
                "type": "array",
                "description": "Up to ten publications in state `with_errors`, newest first.",
                "items": {
                  "$ref": "#/components/schemas/DashboardDashboardPublicationRef"
                }
              },
              "upcoming_publications": {
                "type": "array",
                "description": "Up to ten publications due in the next 48 hours.",
                "items": {
                  "$ref": "#/components/schemas/DashboardDashboardPublicationRef"
                }
              },
              "total_drafts": {
                "type": "integer"
              }
            }
          },
          "publications": {
            "allOf": [
              {
                "$ref": "#/components/schemas/DashboardPublicationsSummary"
              },
              {
                "type": "object",
                "properties": {
                  "previous_total": {
                    "type": "integer",
                    "description": "The same count for the previous period, for the delta."
                  }
                }
              }
            ]
          },
          "publication_metrics": {
            "type": "object",
            "properties": {
              "total": {
                "$ref": "#/components/schemas/NormalizedMetrics"
              },
              "previous_total": {
                "$ref": "#/components/schemas/NormalizedMetrics"
              },
              "by_network": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "social_network": {
                      "$ref": "#/components/schemas/SocialNetwork"
                    },
                    "publications": {
                      "type": "integer"
                    },
                    "metrics": {
                      "$ref": "#/components/schemas/NormalizedMetrics"
                    }
                  }
                }
              },
              "top": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/DashboardTopPublication"
                }
              }
            },
            "required": [
              "total",
              "previous_total",
              "by_network",
              "top"
            ]
          },
          "account_metrics": {
            "type": "object",
            "properties": {
              "total": {
                "$ref": "#/components/schemas/NormalizedMetrics"
              },
              "previous_total": {
                "$ref": "#/components/schemas/NormalizedMetrics"
              },
              "by_day": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/DashboardMetricRow"
                }
              },
              "by_network": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/DashboardMetricRow"
                }
              }
            },
            "required": [
              "total",
              "previous_total",
              "by_day",
              "by_network"
            ]
          },
          "plan_use": {
            "$ref": "#/components/schemas/DashboardPlanUse"
          },
          "ai_plans": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer"
              },
              "credits_spent": {
                "type": "integer"
              },
              "by_state": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "state": {
                      "type": "string"
                    },
                    "total": {
                      "type": "integer"
                    }
                  }
                }
              },
              "last_plan": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/DashboardDashboardAiPlanRef"
                  }
                ],
                "description": "The most recent plan of the organization, **whatever the range**. Absent when there has never been one."
              },
              "generated_publications": {
                "type": "integer",
                "description": "Publications generated by the plans of the range."
              },
              "pending_validation": {
                "type": "integer",
                "description": "Plans already generated and **waiting for someone to validate them**: work paid for that is not publishing anything yet."
              }
            },
            "required": [
              "by_state",
              "total",
              "credits_spent",
              "generated_publications",
              "pending_validation"
            ]
          },
          "messages": {
            "type": "object",
            "properties": {
              "unread": {
                "type": "integer"
              }
            },
            "required": [
              "unread"
            ]
          }
        },
        "required": [
          "range",
          "available_blocks"
        ],
        "description": "Everything the home screen needs, in ONE round trip.\n\n**A missing block is not an error.** Each one is checked against its own permission and omitted when the caller cannot read it, instead of failing the whole request; `available_blocks` says which ones were allowed. A block that is `true` in `available_blocks` and absent from the body means there was no data — except `messages`, which also turns to `false` when the plan does not include chat."
      },
      "DashboardDashboardAiPlanRef": {
        "type": "object",
        "required": [
          "_id",
          "state",
          "creation_date"
        ],
        "description": "The most recent AI plan, projected: enough to tell at a glance whether one is still generating, failed, or is waiting to be validated. `publications` are identifiers here.",
        "properties": {
          "_id": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "enum": [
              "pending",
              "generating",
              "generated",
              "validated",
              "failed",
              "cancelled"
            ]
          },
          "prompt": {
            "type": "string"
          },
          "credits_spent": {
            "type": "integer"
          },
          "publications": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "creation_date": {
            "type": "string",
            "format": "date-time"
          },
          "generation_end_date": {
            "type": "string",
            "format": "date-time"
          },
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "integer"
              },
              "message": {
                "type": "string"
              },
              "data": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        }
      },
      "DashboardDashboardPublicationRef": {
        "type": "object",
        "required": [
          "_id",
          "social_network"
        ],
        "description": "A publication as the health block projects it: a handful of fields, not a whole `Publication`. `state` is not among them — the list it came from already says what state it is in.",
        "properties": {
          "_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "social_network": {
            "$ref": "#/components/schemas/SocialNetwork"
          },
          "publish_date": {
            "type": "string",
            "format": "date-time"
          },
          "creation_date": {
            "type": "string",
            "format": "date-time"
          },
          "publication_errors": {
            "type": "array",
            "description": "Only on the failed ones. Same shape as in a full `Publication`.",
            "items": {
              "type": "object",
              "required": [
                "code",
                "message"
              ],
              "properties": {
                "code": {
                  "type": "integer"
                },
                "message": {
                  "type": "string"
                },
                "data": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          }
        }
      },
      "DashboardDashboardRange": {
        "type": "object",
        "description": "The range that was actually used, plus the previous period of exactly the same length. The previous one is not \"last month\": comparing 30 days against a calendar month would move the delta with the calendar.",
        "properties": {
          "from_date": {
            "type": "string",
            "format": "date-time"
          },
          "to_date": {
            "type": "string",
            "format": "date-time"
          },
          "previous_from_date": {
            "type": "string",
            "format": "date-time"
          },
          "previous_to_date": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "from_date",
          "to_date",
          "previous_from_date",
          "previous_to_date"
        ]
      },
      "DashboardMetricName": {
        "type": "string",
        "description": "A metric in PlanVortex's common vocabulary. Each network reports what it reports and PlanVortex translates it; a metric a network does not publish is absent, never zero.",
        "enum": [
          "impressions",
          "reach",
          "engagement",
          "likes",
          "comments",
          "shares",
          "saves",
          "clicks",
          "video_views",
          "profile_views",
          "followers",
          "followers_gained"
        ]
      },
      "DashboardMetricRow": {
        "type": "object",
        "properties": {
          "group": {
            "type": [
              "string",
              "null"
            ],
            "description": "The value of the axis: the day, the network or the account identifier. **`null` when `group_by` was `total`** — the field is always there, the value is not always a string."
          },
          "name": {
            "$ref": "#/components/schemas/DashboardMetricName"
          },
          "value": {
            "type": "number"
          }
        },
        "required": [
          "group",
          "name",
          "value"
        ]
      },
      "DashboardPlanUse": {
        "type": "object",
        "properties": {
          "actual_use": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PlanData"
              }
            ],
            "description": "What the organization and its children are consuming right now."
          },
          "actual_asigned": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PlanData"
              }
            ],
            "description": "What has been handed down to child organizations out of this organization's plan."
          },
          "limits": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PlanData"
              }
            ],
            "description": "The plan in force. An organization with no plan of its own inherits the closest parent that has one."
          }
        },
        "required": [
          "actual_use",
          "actual_asigned",
          "limits"
        ]
      },
      "DashboardPublicationsSummary": {
        "type": "object",
        "properties": {
          "total": {
            "type": "integer",
            "description": "Publications created in the range."
          },
          "by_state": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "state": {
                  "type": "string"
                },
                "total": {
                  "type": "integer"
                }
              }
            }
          },
          "by_network": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "social_network": {
                  "$ref": "#/components/schemas/SocialNetwork"
                },
                "total": {
                  "type": "integer"
                }
              }
            }
          },
          "by_day": {
            "type": "array",
            "description": "Publications **created** each day, split by state.",
            "items": {
              "type": "object",
              "properties": {
                "day": {
                  "type": "string",
                  "description": "`YYYY-MM-DD`, in UTC."
                },
                "state": {
                  "type": "string"
                },
                "total": {
                  "type": "integer"
                }
              }
            }
          },
          "published_by_day": {
            "type": "array",
            "description": "Publications that actually **went out** each day, split by network. Only the ones in state `sended`.",
            "items": {
              "type": "object",
              "properties": {
                "day": {
                  "type": "string",
                  "description": "`YYYY-MM-DD`, in UTC."
                },
                "social_network": {
                  "$ref": "#/components/schemas/SocialNetwork"
                },
                "total": {
                  "type": "integer"
                }
              }
            }
          }
        },
        "required": [
          "total",
          "by_state",
          "by_network",
          "by_day",
          "published_by_day"
        ]
      },
      "DashboardTopPublication": {
        "type": "object",
        "required": [
          "id_publication",
          "social_network",
          "metrics",
          "publication"
        ],
        "description": "One row of the ranking. It comes out of the stats aggregation, not out of the publications collection, so it does **not** have the shape of a `Publication`: there is no `_id` (the identifier is `id_publication`) and the content travels nested under `publication`.\n\nOnly publications that have already been measured can appear here. For a listing that includes the unmeasured ones, use `GET /organizations/{id}/publications/stats`.",
        "properties": {
          "id_publication": {
            "type": "string"
          },
          "social_network": {
            "$ref": "#/components/schemas/SocialNetwork"
          },
          "publish_date": {
            "type": "string",
            "format": "date-time"
          },
          "collected_date": {
            "type": "string",
            "format": "date-time",
            "description": "When this measurement was taken."
          },
          "metrics": {
            "$ref": "#/components/schemas/NormalizedMetrics"
          },
          "engagement_base": {
            "type": "string",
            "enum": [
              "reach",
              "impressions",
              "followers"
            ],
            "description": "What this row's engagement rate is divided by. **Two rows with different bases are not comparable**, so say which one it is when you put them in the same table."
          },
          "publication": {
            "type": "object",
            "description": "The bit of the publication needed to render the row. Nothing else is projected.",
            "properties": {
              "text": {
                "type": "string"
              },
              "title": {
                "type": "string"
              },
              "files": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Upload"
                }
              },
              "publication_type": {
                "type": "string"
              },
              "url": {
                "type": "string"
              },
              "external_identifier": {
                "type": "string"
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "description": "Error payload returned by every failing request.\n\n**Classify by `code`, never by the HTTP status.** Every domain error travels with HTTP 400 — an expired token, a disconnected account, an exhausted plan quota and a text that is too long are all 400. Only error 520 (permissions) answers 401, and an unexpected failure answers 500.\n\nThe catalogue grows with the product, so treat an unknown `code` as a generic failure instead of rejecting it.",
        "properties": {
          "message": {
            "type": "string",
            "description": "Human readable description of the error."
          },
          "code": {
            "type": "integer",
            "description": "PlanVortex error code. Ranges: 500-544 auth, tokens and client apps · 601-612 user · 700-715 social accounts · 800-810 files · 900-960 publications · 1000-1003 general · 1100-1111 organizations · 1200-1207 roles · 1300-1307 client plan · 1400-1408 organization plan · 1500-1512 messaging · 1600-1601 contacts · 1900-1906 payments · 2000-2099 products · 2100-2199 AI plans · 2200-2299 integrations."
          },
          "data": {
            "additionalProperties": true,
            "type": "object",
            "description": "Extra context attached to the error, when there is any."
          }
        },
        "required": [
          "message",
          "code"
        ]
      },
      "FileProperties": {
        "type": "object",
        "description": "Measurements taken when the file was ingested. Always present on an upload.",
        "required": [
          "width",
          "height",
          "duration",
          "size_in_bytes",
          "aspect_ratio",
          "allowed_social_networks"
        ],
        "properties": {
          "width": {
            "type": "integer",
            "description": "`0` on a file whose dimensions could not be read."
          },
          "height": {
            "type": "integer"
          },
          "duration": {
            "type": "integer",
            "description": "Seconds. `0` on an image."
          },
          "size_in_bytes": {
            "type": "integer"
          },
          "aspect_ratio": {
            "type": "object",
            "description": "Width divided by height, as a number and as a person writes it.",
            "required": [
              "value",
              "text"
            ],
            "properties": {
              "value": {
                "type": "number",
                "example": 1.7777777777777777
              },
              "text": {
                "type": "string",
                "example": "16:9"
              }
            }
          },
          "allowed_social_networks": {
            "type": "array",
            "description": "The networks whose accepted crops this file's ratio matches, with a 0.1 margin. It is about the RATIO only: a network listed here can still reject the file for its duration or its weight (see `GET /social_limits`).",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "IntegrationsGoogleDriveConnectRequest": {
        "type": "object",
        "required": [
          "provider",
          "code"
        ],
        "properties": {
          "provider": {
            "type": "string",
            "enum": [
              "google_drive"
            ]
          },
          "code": {
            "type": "string",
            "description": "OAuth code returned to the redirect of connect_link. Single-use."
          }
        }
      },
      "IntegrationsIntegration": {
        "type": "object",
        "description": "Credentials are never returned. `connected` is the summary the panel paints: false means the connection failed and needs attention.",
        "properties": {
          "_id": {
            "type": "string"
          },
          "id_organization": {
            "type": "string"
          },
          "id_client": {
            "type": "string"
          },
          "provider": {
            "$ref": "#/components/schemas/IntegrationsIntegrationProviderName"
          },
          "name": {
            "type": "string",
            "example": "Drive de ana@empresa.com"
          },
          "external_identifier": {
            "type": "string",
            "description": "The Google account email, or the feed URL.",
            "example": "ana@empresa.com"
          },
          "config": {
            "allOf": [
              {
                "$ref": "#/components/schemas/IntegrationsRssConfig"
              }
            ],
            "description": "Provider-specific configuration. **Empty object for `google_drive`** — the Picker supplies everything — so every field here is optional and only an `rss` integration fills them in."
          },
          "enabled": {
            "type": "boolean",
            "description": "Only enabled integrations consume plan allowance."
          },
          "connected": {
            "type": "boolean",
            "description": "`error_code` is empty. It is computed on the way out, not stored: the panel needs to know whether the connection is alive, not with which credentials."
          },
          "error_code": {
            "type": [
              "integer",
              "null"
            ],
            "description": "PlanVortex error code of the last failure (2203 token revoked, 2205 feed unreachable, 924 no publication allowance left…)."
          },
          "last_used_date": {
            "type": "string",
            "format": "date-time"
          },
          "creation_date": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "_id",
          "id_organization",
          "id_client",
          "provider",
          "name",
          "config",
          "enabled",
          "connected",
          "creation_date"
        ]
      },
      "IntegrationsIntegrationProvider": {
        "type": "object",
        "properties": {
          "provider": {
            "$ref": "#/components/schemas/IntegrationsIntegrationProviderName"
          },
          "requires_oauth": {
            "type": "boolean",
            "description": "true = connect with connect_link + code. false = connect with a form built from config_fields."
          },
          "file_import": {
            "type": "boolean",
            "description": "Contributes files to the library through POST /uploads/import."
          },
          "content_feed": {
            "type": "boolean",
            "description": "Polled by the poll-feeds job, which turns new entries into publications."
          },
          "accepted_formats": {
            "type": "array",
            "description": "File formats accepted at the door. **Empty when `file_import` is false** — that is what `rss` returns, and it does not mean \"anything goes\". heic/heif are accepted and converted to JPEG on ingestion, so what ends up stored is always jpeg.",
            "items": {
              "type": "string"
            },
            "example": [
              "mp4",
              "jpeg",
              "gif",
              "png",
              "jpg",
              "heic",
              "heif"
            ]
          },
          "config_fields": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "type": {
                  "type": "string",
                  "enum": [
                    "url",
                    "text",
                    "textarea",
                    "boolean",
                    "accounts",
                    "select"
                  ]
                },
                "required": {
                  "type": "boolean"
                },
                "default": {},
                "options": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              },
              "required": [
                "name",
                "type"
              ]
            }
          }
        },
        "required": [
          "provider",
          "requires_oauth",
          "file_import",
          "content_feed",
          "accepted_formats",
          "config_fields"
        ]
      },
      "IntegrationsIntegrationProviderName": {
        "type": "string",
        "enum": [
          "google_drive",
          "rss"
        ]
      },
      "IntegrationsRssConfig": {
        "type": "object",
        "description": "Provider-specific configuration. Empty for google_drive.",
        "properties": {
          "url": {
            "type": "string"
          },
          "id_accounts": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "publication_type": {
            "type": "string"
          },
          "template": {
            "type": "string"
          },
          "auto_publish": {
            "type": "boolean"
          },
          "import_image": {
            "type": "boolean"
          },
          "seen_guids": {
            "type": "array",
            "description": "Entries already processed (last 200, FIFO). Owned by the job: filled at connection time with everything the feed already had, so the back catalogue is never published.",
            "items": {
              "type": "string"
            }
          },
          "last_checked": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "IntegrationsRssConnectRequest": {
        "type": "object",
        "required": [
          "provider",
          "url",
          "id_accounts"
        ],
        "properties": {
          "provider": {
            "type": "string",
            "enum": [
              "rss"
            ]
          },
          "url": {
            "type": "string",
            "description": "Public feed URL (RSS 2.0 or Atom). Private or authenticated feeds are not supported.",
            "example": "https://blog.cliente.com/feed"
          },
          "id_accounts": {
            "type": "array",
            "description": "Accounts of this organization the entries will be published to. At least one, otherwise 2206.",
            "items": {
              "type": "string"
            }
          },
          "publication_type": {
            "type": "string",
            "description": "Optional; defaults to `\"profile\"`."
          },
          "template": {
            "type": "string",
            "description": "Text template. Placeholders: {{title}}, {{link}}, {{summary}}. The result is truncated to the character limit of each network, taken from GET /social_limits. Optional; defaults to `\"{{title}}\\n\\n{{link}}\"`."
          },
          "auto_publish": {
            "type": "boolean",
            "description": "false (the default) creates each entry as a draft for review. true schedules it a few minutes out, so there is a window to catch it before it goes out on the client's networks. Optional; defaults to `false`."
          },
          "import_image": {
            "type": "boolean",
            "description": "Import the entry's featured image (enclosure, media:content or the first <img> of the content) into the library and attach it. Optional; defaults to `true`."
          }
        }
      },
      "Message": {
        "type": "object",
        "description": "A message exchanged with a contact.\n\n**Careful with the three reference fields.** `contact_id`, `from_contact_id` and `message_options.files` arrive **populated** — the whole object, not the identifier — in the messages list and in the webhook PlanVortex posts to your app, and as plain identifiers everywhere else. The types say `string | object` because both really happen.",
        "properties": {
          "_id": {
            "type": "string"
          },
          "id_account": {
            "type": "string"
          },
          "contact_id": {
            "description": "Set when **we** wrote to the contact. Exactly one of `contact_id` and `from_contact_id` is present, and which one tells you the direction of the message.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "$ref": "#/components/schemas/Contact"
              }
            ]
          },
          "from_contact_id": {
            "description": "Set when the **contact** wrote to us.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "$ref": "#/components/schemas/Contact"
              }
            ]
          },
          "read": {
            "type": "boolean"
          },
          "text": {
            "type": "string",
            "description": "Absent on the messages that carry no text of their own, such as a `messaging_seen` acknowledgement."
          },
          "message_type": {
            "$ref": "#/components/schemas/MessageType"
          },
          "message_options": {
            "$ref": "#/components/schemas/MessageOptions"
          },
          "message_errors": {
            "type": "array",
            "description": "Why the network refused this message, if it did. Same shape as an API error. Empty when nothing went wrong.",
            "items": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "in_response_to": {
            "type": "string",
            "description": "Identifier of the PlanVortex message this one answers."
          },
          "in_response_external_id": {
            "type": "string",
            "description": "Identifier on the network of the publication, comment or message this one answers."
          },
          "element_external_id": {
            "type": "string",
            "description": "Identifier of this message on the network."
          },
          "creation_date": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "_id",
          "id_account",
          "read",
          "message_type",
          "message_options",
          "message_errors",
          "creation_date"
        ]
      },
      "MessageOptions": {
        "type": "object",
        "description": "Everything a message can carry besides its text. Which block is required depends on `message_type`.",
        "properties": {
          "files": {
            "description": "The uploads attached to the message. **Populated** wherever the message itself is: the messages list and webhook deliveries carry whole uploads, everything else carries their identifiers. Handle both.",
            "type": "array",
            "items": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "$ref": "#/components/schemas/Upload"
                }
              ]
            }
          },
          "files_urls": {
            "type": "array",
            "description": "Files already hosted somewhere else. Filled in by PlanVortex when a message arrives from the network.",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string"
                },
                "mime_type": {
                  "type": "string"
                }
              }
            }
          },
          "template_name": {
            "type": "string",
            "description": "Required for `template_message`."
          },
          "template_language": {
            "type": "string",
            "description": "Required for `template_message`, as the network's language code."
          },
          "payload": {
            "type": "string",
            "description": "Payload of a Meta postback."
          },
          "metaElements": {
            "type": "array",
            "description": "Meta cards, for `elements_message` and `button_message`.",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "metaQuickReplies": {
            "type": "array",
            "description": "Meta quick replies, for `quick_reply_message`.",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "whatsappInteractive": {
            "type": "object",
            "description": "WhatsApp interactive list, for `interactive_message`. It needs at least one section.",
            "additionalProperties": true
          }
        },
        "required": [
          "files",
          "files_urls"
        ]
      },
      "MessageType": {
        "type": "string",
        "description": "What kind of message this is. `simple_message` and `file_message` work everywhere; the rest are network-specific shapes.",
        "enum": [
          "simple_message",
          "file_message",
          "comment_message",
          "publication_message",
          "quick_reply_message",
          "button_message",
          "elements_message",
          "postback_message",
          "template_message",
          "interactive_message"
        ]
      },
      "MessagesConversation": {
        "type": "object",
        "description": "One contact's thread, as it looks in an inbox list.",
        "properties": {
          "contact": {
            "$ref": "#/components/schemas/Contact"
          },
          "date": {
            "type": "string",
            "format": "date-time",
            "description": "When the last message of the thread was written, which is what the list is sorted by."
          },
          "unread_messages": {
            "type": "integer",
            "description": "Unread messages **from the contact**. Ours never count."
          }
        },
        "required": [
          "contact",
          "date",
          "unread_messages"
        ]
      },
      "MessagesConversationTotals": {
        "description": "Two different answers, not one with optional fields: **without** `group_by` you get `{total}`, **with** it you get `{stats, group}`. Never both.",
        "oneOf": [
          {
            "type": "object",
            "required": [
              "total"
            ],
            "properties": {
              "total": {
                "type": "integer",
                "description": "Conversations in the range. A conversation is one contact on one day, so the same person writing on Monday and on Tuesday counts twice."
              }
            }
          },
          {
            "type": "object",
            "required": [
              "stats",
              "group"
            ],
            "properties": {
              "group": {
                "type": "string",
                "enum": [
                  "day",
                  "month",
                  "year"
                ],
                "description": "The grouping that was applied."
              },
              "stats": {
                "type": "array",
                "description": "The series, sorted ascending. Empty when the range has no conversations.",
                "items": {
                  "type": "object",
                  "required": [
                    "groupValue",
                    "totalConversations"
                  ],
                  "properties": {
                    "groupValue": {
                      "type": "integer",
                      "description": "The **number** Mongo's `$dayOfYear` / `$month` / `$year` gives, not a date: 240 for day, 8 for month, 2026 for year. Two years in the same `day` series collide on the same value — narrow the range instead."
                    },
                    "totalConversations": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "MessagesMessageInput": {
        "type": "object",
        "required": [
          "message_type"
        ],
        "properties": {
          "message_type": {
            "$ref": "#/components/schemas/MessageType"
          },
          "text": {
            "type": "string",
            "description": "Required for the text-based types. Validated against `characters` in `GET /social_limits`."
          },
          "message_options": {
            "$ref": "#/components/schemas/MessageOptions"
          },
          "in_response_external_id": {
            "type": "string",
            "description": "Required by `comment_message` and `publication_message`, and ignored by every other type (error `1510` when it is missing). It is the identifier the NETWORK gives: a comment's `external_id` or a publication's `external_identifier`, never a PlanVortex `_id`.\n\nOnly Facebook and Instagram do anything with it: `comment_message` sends a private reply to a public comment (Meta's `recipient.comment_id`) and `publication_message` attaches the post as a `MEDIA_SHARE`."
          }
        },
        "description": "What you send to write a message.\n\n`comment_message` and `publication_message` need `in_response_external_id`, the identifier of the comment or the publication being answered ON THE NETWORK. The endpoint did not read it from the body until 2026-08-24, which left both types unreachable from the public API; it does now."
      },
      "NormalizedMetrics": {
        "type": "object",
        "description": "Metrics translated to a **common vocabulary** shared by every network, which is what makes two networks comparable and summable (each network names them differently: `page_post_engagements`, `total_interactions`, `views`…).\n\n**A missing key means the network does not publish that metric** — it is never an implicit zero. A key present with value `0` means it was measured and came out zero. Never default a missing key to 0 when displaying it.\n\n`engagement` is the network's own total when it provides one, and otherwise the sum of likes, comments, shares, saves and clicks. Video views are deliberately excluded from it: a view is not an interaction.",
        "properties": {
          "impressions": {
            "type": "integer",
            "description": "Times the content was shown (not unique)"
          },
          "reach": {
            "type": "integer",
            "description": "Unique users reached"
          },
          "engagement": {
            "type": "integer",
            "description": "Total interactions"
          },
          "likes": {
            "type": "integer"
          },
          "comments": {
            "type": "integer"
          },
          "shares": {
            "type": "integer"
          },
          "saves": {
            "type": "integer"
          },
          "clicks": {
            "type": "integer"
          },
          "video_views": {
            "type": "integer"
          },
          "profile_views": {
            "type": "integer"
          },
          "followers": {
            "type": "integer",
            "description": "Followers accumulated at that date, not the day's gain."
          },
          "followers_gained": {
            "type": "integer",
            "description": "Followers gained that day."
          }
        }
      },
      "Organization": {
        "type": "object",
        "description": "The container of accounts, publications and files. Organizations can nest.",
        "required": [
          "_id",
          "id_client",
          "name",
          "creation_date"
        ],
        "properties": {
          "_id": {
            "type": "string",
            "example": "66d04a6a427f4c43b9d97f54"
          },
          "id_client": {
            "type": "string",
            "description": "The client this organization belongs to."
          },
          "name": {
            "type": "string"
          },
          "parent_organization": {
            "type": "string",
            "description": "The organization this one hangs from. Absent on a root organization."
          },
          "creation_date": {
            "type": "string",
            "format": "date-time"
          },
          "actual_plan": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PlanData"
              }
            ],
            "description": "The slice of the client's plan assigned to this organization. **Absent when nothing was assigned**, and then the organization shares whatever its nearest parent with a plan has — or, failing that, the client's unassigned remainder. Ask `GET /organizations/{id_organization}/limits` for the effective numbers instead of reading this."
          },
          "actual_use": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PlanData"
              }
            ],
            "description": "Current consumption. **Only present with `getUse=true`.** `twitter_credits` and `ai_credits` are what has been spent in the current calendar month; the rest is what exists right now."
          },
          "actual_asigned": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PlanData"
              }
            ],
            "description": "What is already handed out to the organizations sharing this plan, which is what is left to assign. **Only present with `getUse=true`.**"
          },
          "ai_context": {
            "$ref": "#/components/schemas/AiContext"
          },
          "stats_settings": {
            "$ref": "#/components/schemas/StatsSettings"
          },
          "social_credentials": {
            "type": "object",
            "description": "The organization's own application credentials, by network. A network that is absent is not configured, and then it does not even appear as connectable.",
            "properties": {
              "discord": {
                "$ref": "#/components/schemas/SocialCredentials"
              }
            }
          }
        }
      },
      "OrganizationsLimit": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PlanData"
          }
        ],
        "description": "What this organization may actually use. An organization with no plan of its own inherits the nearest parent's, and failing that the share of the client's plan that is not assigned to anyone."
      },
      "OrganizationsOrganizationCreate": {
        "type": "object",
        "description": "Body accepted when creating an organization. Only these properties are read.",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Organization name."
          },
          "actual_plan": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PlanData"
              }
            ],
            "description": "Resources assigned to this organization, taken from what the client has contracted."
          }
        }
      },
      "OrganizationsOrganizationList": {
        "type": "object",
        "properties": {
          "organizations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Organization"
            }
          },
          "total": {
            "type": "integer"
          }
        },
        "required": [
          "organizations",
          "total"
        ]
      },
      "OrganizationsOrganizationOne": {
        "type": "object",
        "properties": {
          "organization": {
            "$ref": "#/components/schemas/Organization"
          }
        },
        "required": [
          "organization"
        ]
      },
      "OrganizationsOrganizationUpdate": {
        "type": "object",
        "description": "Body accepted when updating an organization. NOTE: `name` and `parent_organization` are NOT updatable through this endpoint - the server keeps their current value and silently ignores them.",
        "properties": {
          "actual_plan": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PlanData"
              }
            ],
            "description": "New resource assignment. Validated against what is left of the client's contracted plan; lowering it below what the organization already uses is rejected."
          },
          "stats_settings": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StatsSettings"
              }
            ],
            "description": "Statistics collection settings."
          }
        }
      },
      "OrganizationsRolesOrganizationRole": {
        "type": "object",
        "description": "An organization role as the API returns it. Note it exposes `total_users` and never the list of user identifiers.",
        "properties": {
          "_id": {
            "type": "string",
            "description": "Role identifier."
          },
          "id_organization": {
            "type": "string",
            "description": "Organization the role belongs to."
          },
          "name": {
            "type": "string",
            "description": "Role name."
          },
          "default": {
            "type": "boolean",
            "description": "Whether this is a default role. Default roles cannot be updated or deleted, and their last user cannot be removed."
          },
          "permissions": {
            "type": "array",
            "description": "Organization permissions granted by this role.",
            "items": {
              "type": "string",
              "enum": [
                "organization:create",
                "organization:update",
                "organization:read",
                "organization:delete",
                "organization_roles:create",
                "organization_roles:update",
                "organization_roles:read",
                "organization_roles:delete",
                "organization_users_roles:read",
                "organization_users_roles:create",
                "organization_users_roles:delete",
                "accounts:create",
                "accounts:update",
                "accounts:read",
                "accounts:delete",
                "products:create",
                "products:update",
                "products:read",
                "products:delete",
                "files:create",
                "files:update",
                "files:read",
                "files:delete",
                "publications:create",
                "publications:update",
                "publications:read",
                "publications:delete",
                "account_stats:read",
                "publication_stats:read",
                "messages:create",
                "messages:read",
                "messages:delete",
                "contacts:create",
                "contacts:read",
                "contacts:update",
                "contacts:delete",
                "ai_plans:create",
                "ai_plans:read",
                "ai_plans:update",
                "ai_plans:delete"
              ]
            }
          },
          "total_users": {
            "type": "integer",
            "description": "Number of users in the role."
          },
          "creation_date": {
            "type": "string",
            "format": "date-time",
            "description": "When the role was created."
          }
        }
      },
      "OrganizationsRolesOrganizationRoleInput": {
        "type": "object",
        "description": "Name and permission list of an organization role. Both properties overwrite the stored value.",
        "required": [
          "name",
          "permissions"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Role name. Must be unique inside the organization."
          },
          "permissions": {
            "type": "array",
            "description": "Complete list of organization permissions granted by this role.",
            "items": {
              "type": "string",
              "enum": [
                "organization:create",
                "organization:update",
                "organization:read",
                "organization:delete",
                "organization_roles:create",
                "organization_roles:update",
                "organization_roles:read",
                "organization_roles:delete",
                "organization_users_roles:read",
                "organization_users_roles:create",
                "organization_users_roles:delete",
                "accounts:create",
                "accounts:update",
                "accounts:read",
                "accounts:delete",
                "products:create",
                "products:update",
                "products:read",
                "products:delete",
                "files:create",
                "files:update",
                "files:read",
                "files:delete",
                "publications:create",
                "publications:update",
                "publications:read",
                "publications:delete",
                "account_stats:read",
                "publication_stats:read",
                "messages:create",
                "messages:read",
                "messages:delete",
                "contacts:create",
                "contacts:read",
                "contacts:update",
                "contacts:delete",
                "ai_plans:create",
                "ai_plans:read",
                "ai_plans:update",
                "ai_plans:delete"
              ]
            }
          }
        }
      },
      "OrganizationsRolesOrganizationRoleList": {
        "type": "object",
        "properties": {
          "roles": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrganizationsRolesOrganizationRole"
            }
          },
          "total": {
            "type": "integer",
            "description": "Total number of roles in the organization, ignoring pagination."
          }
        }
      },
      "OrganizationsRolesOrganizationRoleOne": {
        "type": "object",
        "properties": {
          "rol": {
            "$ref": "#/components/schemas/OrganizationsRolesOrganizationRole"
          }
        }
      },
      "OrganizationsRolesPermissionList": {
        "type": "object",
        "properties": {
          "permissions": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "organization:create",
                "organization:update",
                "organization:read",
                "organization:delete",
                "organization_roles:create",
                "organization_roles:update",
                "organization_roles:read",
                "organization_roles:delete",
                "organization_users_roles:read",
                "organization_users_roles:create",
                "organization_users_roles:delete",
                "accounts:create",
                "accounts:update",
                "accounts:read",
                "accounts:delete",
                "products:create",
                "products:update",
                "products:read",
                "products:delete",
                "files:create",
                "files:update",
                "files:read",
                "files:delete",
                "publications:create",
                "publications:update",
                "publications:read",
                "publications:delete",
                "account_stats:read",
                "publication_stats:read",
                "messages:create",
                "messages:read",
                "messages:delete",
                "contacts:create",
                "contacts:read",
                "contacts:update",
                "contacts:delete",
                "ai_plans:create",
                "ai_plans:read",
                "ai_plans:update",
                "ai_plans:delete"
              ]
            }
          }
        }
      },
      "OrganizationsRolesUserInOrganization": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "User identifier."
          },
          "username": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "enabled": {
            "type": "boolean",
            "description": "`false` while an invitation is still pending acceptance."
          },
          "firstname": {
            "type": "string"
          },
          "lastname": {
            "type": "string"
          },
          "roles": {
            "type": "array",
            "description": "Roles this user holds in the organization.",
            "items": {
              "type": "object",
              "properties": {
                "_id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "OrganizationsRolesUserInOrganizationList": {
        "type": "object",
        "properties": {
          "users": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrganizationsRolesUserInOrganization"
            }
          },
          "total": {
            "type": "integer",
            "description": "Total number of users, ignoring pagination. May be higher than the length of `users` if some user no longer exists in the identity provider."
          }
        }
      },
      "OrganizationsSocialCredentialsInput": {
        "type": "object",
        "description": "Credentials of the organization's own Discord application. **Write-only**: nothing sent here ever comes back.\n\nAll three are required the first time. Afterwards, what you leave out is kept — so the `client_id` can be fixed without resending the secrets.",
        "properties": {
          "client_id": {
            "type": "string",
            "description": "The Discord application's id (its *Application ID*, which is also its client id)."
          },
          "client_secret": {
            "type": "string",
            "description": "The application's OAuth2 secret. Stored encrypted."
          },
          "bot_token": {
            "type": "string",
            "description": "The bot's token. Stored encrypted, and validated against Discord before anything is saved."
          }
        }
      },
      "OrganizationsUser": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "username": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "firstname": {
            "type": "string"
          },
          "lastname": {
            "type": "string"
          }
        }
      },
      "OrganizationsUserList": {
        "type": "object",
        "properties": {
          "users": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrganizationsUser"
            }
          },
          "total": {
            "type": "integer"
          }
        }
      },
      "PlanData": {
        "type": "object",
        "description": "The resources a plan grants. On a client it is what was contracted; on an organization, the slice of it that was assigned. The sum across all the organizations of a client can never exceed what the client has contracted.",
        "required": [
          "accounts",
          "publications",
          "users",
          "space",
          "integrations"
        ],
        "properties": {
          "accounts": {
            "type": "integer",
            "description": "Social accounts that may be connected."
          },
          "publications": {
            "type": "integer",
            "description": "Publications that may be sent per month."
          },
          "users": {
            "type": "integer",
            "description": "Users with access."
          },
          "space": {
            "type": "number",
            "description": "Storage, in GB."
          },
          "integrations": {
            "type": "integer",
            "description": "Connections to a third-party tool material is pulled from (Google Drive, an RSS feed). Not the same thing as an app: an app is API access, and it is a Custom-plan feature of its own."
          },
          "twitter_credits": {
            "type": "integer",
            "description": "Monthly X (Twitter) credits. X bills per use: 15 per post, 200 if the text contains a link, 15 per deletion, 1 per stat read, 1 per timeline item. The pool resets on the 1st of each calendar month and does not roll over."
          },
          "ai_credits": {
            "type": "integer",
            "description": "Monthly AI credits (1 credit = $0.001 of provider cost): 15 per plan orchestration pass, 2 per generated text, 70 per generated image. Resets monthly and does not roll over. A client using its own provider key (BYOK) does not consume them in that scope."
          },
          "artificial_inteligence": {
            "type": "boolean",
            "description": "Whether AI generation is enabled."
          },
          "whatsapp": {
            "type": "boolean",
            "description": "Whether WhatsApp may be connected."
          },
          "stats": {
            "type": "boolean",
            "description": "Whether statistics collection is enabled."
          }
        }
      },
      "ProductsProduct": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ProductsProductInput"
          }
        ],
        "description": "A product as the network returns it. `id` is always present here."
      },
      "ProductsProductCatalog": {
        "type": "object",
        "description": "A Meta commerce catalogue, as the network returns it.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "product_count": {
            "type": "integer"
          },
          "feed_count": {
            "type": "integer"
          },
          "default_image_url": {
            "type": "string"
          },
          "is_local_catalog": {
            "type": "boolean"
          },
          "is_catalog_segment": {
            "type": "boolean"
          }
        },
        "additionalProperties": true
      },
      "ProductsProductCatalogInput": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string"
          }
        }
      },
      "ProductsProductInput": {
        "type": "object",
        "description": "A product in Meta's Commerce vocabulary. Only the fields PlanVortex depends on are listed; anything else Meta accepts travels through untouched.",
        "required": [
          "name",
          "price",
          "currency",
          "image_url",
          "retailer_id"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Send it to update an existing product. Leave it out to create one."
          },
          "retailer_id": {
            "type": "string",
            "description": "**Your** identifier for the product. It is what ties the Meta catalogue to your system."
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "price": {
            "type": "integer",
            "description": "In **cents** of `currency`. 1250 is 12,50."
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217 code, for example `EUR`."
          },
          "image_url": {
            "type": "string"
          },
          "additional_image_urls": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "url": {
            "type": "string",
            "description": "The product's page on your site."
          },
          "availability": {
            "type": "string",
            "enum": [
              "in stock",
              "out of stock",
              "preorder",
              "available for order",
              "discontinued",
              "pending",
              "mark_as_sold"
            ]
          },
          "condition": {
            "type": "string",
            "enum": [
              "new",
              "refurbished",
              "used",
              "used_like_new",
              "used_good",
              "used_fair",
              "cpo",
              "open_box_new"
            ]
          },
          "brand": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "color": {
            "type": "string"
          }
        },
        "additionalProperties": true
      },
      "Publication": {
        "type": "object",
        "required": [
          "_id",
          "id_organization",
          "id_account",
          "social_network",
          "publication_type",
          "state",
          "files",
          "publication_errors",
          "retries",
          "creation_date"
        ],
        "properties": {
          "_id": {
            "type": "string"
          },
          "id_organization": {
            "type": "string"
          },
          "id_account": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "$ref": "#/components/schemas/Account"
              }
            ],
            "description": "The account the publication goes out through.\n\n**It is not always the same shape.** The single-publication operations (create, read, retry) return the account already resolved; the listing and the update return its identifier as a string. Check before using it."
          },
          "social_network": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SocialNetwork"
              }
            ],
            "description": "Always the network of the account in `id_account`."
          },
          "name": {
            "type": "string",
            "description": "Internal name. Never shown on the social network."
          },
          "text": {
            "type": "string"
          },
          "title": {
            "type": "string",
            "description": "Only the networks that have a title field use it."
          },
          "publication_type": {
            "type": "string",
            "enum": [
              "profile",
              "page",
              "group",
              "reels",
              "stories",
              "message"
            ]
          },
          "state": {
            "type": "string",
            "enum": [
              "ready",
              "withErrors",
              "sended",
              "draft",
              "publishing"
            ],
            "description": "`draft` is never sent; `ready` is scheduled; `publishing` is in the network's hands right now; `sended` went out; `withErrors` failed and carries the reason in `publication_errors`."
          },
          "files": {
            "type": "array",
            "description": "The attached files, **already resolved**: every read and write path returns full uploads, not identifiers. Identifiers are what you SEND (see `PublicationInput.files`).",
            "items": {
              "$ref": "#/components/schemas/Upload"
            }
          },
          "publish_date": {
            "type": "string",
            "format": "date-time"
          },
          "creation_date": {
            "type": "string",
            "format": "date-time"
          },
          "publication_errors": {
            "type": "array",
            "description": "Why the publication failed, one entry per problem. **It is an array**, and it is empty on a publication that has not failed.\n\nFor a scheduled X (Twitter) publication that runs out of credits at publish time, `code` is 940 and `data` is `{ used, limit }`; the publication stays in state `withErrors` and the client-app webhook is fired.",
            "items": {
              "type": "object",
              "required": [
                "code",
                "message"
              ],
              "properties": {
                "code": {
                  "type": "integer"
                },
                "message": {
                  "type": "string"
                },
                "data": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "retries": {
            "type": "integer",
            "description": "Manual retries already spent on a failed publication, against the `max_retries` published by `GET /publication_limits`. Only `POST .../retry` increases it; updating the publication resets it to 0."
          },
          "external_identifier": {
            "type": "string",
            "description": "The network's own identifier, once published."
          },
          "url": {
            "type": "string",
            "description": "Link to the publication on the network, when there is one."
          },
          "statistics": {
            "$ref": "#/components/schemas/PublicationStats"
          },
          "metrics": {
            "allOf": [
              {
                "$ref": "#/components/schemas/NormalizedMetrics"
              }
            ],
            "description": "Last known measurement, in the common vocabulary. Absent until it is measured."
          },
          "engagement_base": {
            "type": "string",
            "enum": [
              "reach",
              "impressions",
              "followers"
            ],
            "description": "What this publication's engagement rate is divided by."
          },
          "stats_updated_date": {
            "type": "string",
            "format": "date-time"
          },
          "next_stats_update": {
            "type": "string",
            "format": "date-time"
          },
          "id_integration": {
            "type": "string",
            "description": "The integration this publication came from — an RSS feed, for instance. Set when it is created and never changed afterwards."
          }
        }
      },
      "PublicationStats": {
        "type": "object",
        "description": "Raw, per-network metrics for a publication. Only the fields that belong to the publication's own social network are returned.\n\n**An absent field is not a zero.** A field is present only when the network actually reported it; `0` means the network measured zero. This matters most on X (Twitter), where metrics are split into groups with different access levels: `public_metrics` (likes, replys, retwets, quotes, bookmarks, impressions) is always available, while clicks, the pre-computed `engagement` and the video playback quartiles come from X's non-public metrics — only for your own posts, within 30 days of publishing, and only if the app is entitled to them. When they are unavailable they are omitted rather than returned as `0`. Do not default missing fields to zero when displaying them.\n\nOn `discord` there are only two: `likes` (the reactions on the message) and `comments` (the messages in its thread). There is no impressions figure anywhere in Discord's API, so engagement is computed over the server's member count.\n\nOn `bluesky` there are no impressions and no reach either — only the public counters — so engagement is computed over followers.",
        "properties": {
          "likes": {
            "type": "integer"
          },
          "loves": {
            "type": "integer"
          },
          "wows": {
            "type": "integer"
          },
          "hahas": {
            "type": "integer"
          },
          "sorrys": {
            "type": "integer"
          },
          "angers": {
            "type": "integer"
          },
          "impressions": {
            "type": "integer"
          },
          "negative_feedback": {
            "type": "integer"
          },
          "clicks": {
            "type": "integer"
          },
          "page_likes": {
            "type": "integer"
          },
          "video_views": {
            "type": "integer"
          },
          "comments": {
            "type": "integer"
          },
          "follows": {
            "type": "integer"
          },
          "profile_activity": {
            "type": "integer"
          },
          "profile_visits": {
            "type": "integer"
          },
          "shares": {
            "type": "integer"
          },
          "reach": {
            "type": "integer"
          },
          "saved": {
            "type": "integer"
          },
          "retwets": {
            "type": "integer"
          },
          "replys": {
            "type": "integer"
          },
          "quotes": {
            "type": "integer"
          },
          "url_link_clicks": {
            "type": "integer",
            "description": "X (Twitter). Clicks on links in the post. Comes from X's non-public metrics: omitted when unavailable."
          },
          "user_profile_clicks": {
            "type": "integer",
            "description": "X (Twitter). Clicks on the author's profile from the post. Comes from X's non-public metrics: omitted when unavailable."
          },
          "bookmarks": {
            "type": "integer",
            "description": "X (Twitter). Times the post was saved to bookmarks. Always available (`public_metrics`). Normalised as `saves` and counted towards engagement."
          },
          "engagement": {
            "type": "integer",
            "description": "Total interactions. LinkedIn reports it directly. On X it comes from the non-public metrics and, when present, takes precedence over the sum of the individual interactions — it includes interactions the API does not break down."
          },
          "playback_0_count": {
            "type": "integer",
            "description": "X (Twitter). Video playbacks that reached 0% — i.e. started. Comes from X's non-public metrics: omitted when unavailable."
          },
          "playback_25_count": {
            "type": "integer",
            "description": "X (Twitter). Video playbacks that reached 25%. Comes from X's non-public metrics: omitted when unavailable."
          },
          "playback_50_count": {
            "type": "integer",
            "description": "X (Twitter). Video playbacks that reached 50%. Comes from X's non-public metrics: omitted when unavailable."
          },
          "playback_75_count": {
            "type": "integer",
            "description": "X (Twitter). Video playbacks that reached 75%. Comes from X's non-public metrics: omitted when unavailable."
          },
          "playback_100_count": {
            "type": "integer",
            "description": "X (Twitter). Video playbacks that reached 100%. Comes from X's non-public metrics: omitted when unavailable."
          },
          "share": {
            "type": "integer"
          },
          "shareMentions": {
            "type": "integer"
          },
          "views": {
            "type": "integer"
          }
        }
      },
      "PublicationsPublicationInput": {
        "type": "object",
        "description": "Body accepted when creating or updating a publication. Only these properties are read; anything else in the payload is ignored.",
        "properties": {
          "social_network": {
            "type": "string",
            "enum": [
              "facebook",
              "instagram",
              "twitter",
              "linkedin",
              "tiktok",
              "whatsapp",
              "youtube",
              "bluesky",
              "discord"
            ],
            "description": "Network the publication targets. **Required when creating**: the request fails with error 702 if it is missing or not one of these values. It must match the network of the account in the path.\n\nNot every connectable network publishes — a local business listing receives reviews, not posts — so this list is shorter than the one in `GET /social_networks`. Ask `GET /allowed_social_publications` rather than hardcoding it, because it grows."
          },
          "text": {
            "type": "string",
            "description": "Body text of the publication. Either `text` or at least one entry in `files` is required: if both are empty the publication is still created, but in state `withErrors` with `publication_errors[].code = 915`. Maximum length depends on the network. On YouTube this is the video **description** (5,000 characters), and the publication must carry exactly one video file and no images — otherwise it is created in state `withErrors` with `publication_errors[].code = 943`. For X (Twitter), a text containing a link costs 200 credits instead of 15."
          },
          "title": {
            "type": "string",
            "description": "Title for the publication. Only some networks use it: optional on LinkedIn, and **required on YouTube**, where it is the video title and must be 100 characters or fewer — a publication without it, or with a longer one, is created in state `withErrors` with `publication_errors[].code = 944`."
          },
          "files": {
            "type": "array",
            "description": "Identifiers of uploads previously created through the uploads endpoints, attached to this publication.",
            "items": {
              "type": "string"
            }
          },
          "publish_date": {
            "type": "string",
            "format": "date-time",
            "description": "When the publication must go out. If omitted, it is published immediately. An invalid date returns error 938."
          },
          "name": {
            "type": "string",
            "description": "Internal name for the publication. Useful for grouping; never shown on the social network."
          },
          "publication_type": {
            "type": "string",
            "enum": [
              "profile",
              "page",
              "group",
              "reels",
              "stories"
            ],
            "description": "Defaults to `profile`. Not every network accepts every type, and an unsupported combination returns error 923. Allowed values are: facebook and instagram -> profile, reels, stories; twitter, linkedin, tiktok and youtube -> profile; whatsapp -> stories. YouTube has no separate type for Shorts: any vertical video of 3 minutes or less is classified as one automatically."
          },
          "state": {
            "type": "string",
            "enum": [
              "ready",
              "withErrors",
              "sended",
              "draft",
              "publishing"
            ],
            "description": "Send `draft` to store the publication without publishing it. If omitted, the state is resolved automatically: `ready` when everything validates, `withErrors` otherwise. Forcing `sended` marks it as published without actually sending it."
          }
        }
      },
      "PublicationsPublicationList": {
        "type": "object",
        "properties": {
          "publications": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Publication"
            }
          },
          "total": {
            "type": "integer"
          }
        },
        "required": [
          "publications",
          "total"
        ]
      },
      "PublicationsPublicationOne": {
        "type": "object",
        "properties": {
          "publication": {
            "$ref": "#/components/schemas/Publication"
          }
        },
        "required": [
          "publication"
        ]
      },
      "PublicationsPublicationRetry": {
        "type": "object",
        "properties": {
          "publication": {
            "$ref": "#/components/schemas/Publication"
          },
          "max_retries": {
            "type": "integer",
            "description": "Retries a failed publication accepts in total. Read it from here instead of hardcoding it: it is the same number the server enforces."
          }
        },
        "required": [
          "publication",
          "max_retries"
        ]
      },
      "PublicationsPublicationStatsHistory": {
        "type": "object",
        "description": "A publication's measured history plus its last known values.",
        "properties": {
          "id_publication": {
            "type": "string"
          },
          "social_network": {
            "$ref": "#/components/schemas/SocialNetwork"
          },
          "publish_date": {
            "type": "string",
            "format": "date-time"
          },
          "stats_updated_date": {
            "type": "string",
            "format": "date-time",
            "description": "Last time it was measured. Absent means never"
          },
          "next_stats_update": {
            "type": "string",
            "format": "date-time",
            "description": "When the collector will look again. Absent means the 30-day window is over"
          },
          "metrics": {
            "$ref": "#/components/schemas/NormalizedMetrics"
          },
          "engagement_base": {
            "type": "string",
            "enum": [
              "reach",
              "impressions",
              "followers"
            ]
          },
          "statistics": {
            "$ref": "#/components/schemas/PublicationStats"
          },
          "series": {
            "type": "array",
            "description": "One row per measured day, oldest first. Empty is valid: nothing has been measured yet.",
            "items": {
              "$ref": "#/components/schemas/PublicationsPublicationStatsPoint"
            }
          },
          "latest": {
            "type": "object",
            "description": "The most recent row of the series, with the network's raw payload attached. Absent when the series is empty.",
            "required": [
              "collected_date",
              "metrics"
            ],
            "properties": {
              "collected_date": {
                "type": "string",
                "format": "date-time"
              },
              "metrics": {
                "$ref": "#/components/schemas/NormalizedMetrics"
              },
              "engagement_base": {
                "type": "string",
                "enum": [
                  "reach",
                  "impressions",
                  "followers"
                ]
              },
              "raw": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/PublicationStats"
                  }
                ],
                "description": "What the network answered, unprocessed."
              }
            }
          }
        },
        "required": [
          "id_publication",
          "social_network",
          "series"
        ]
      },
      "PublicationsPublicationStatsPoint": {
        "type": "object",
        "description": "One measurement of a publication. `metrics` is the **running total** at `collected_date`, not that day's increment.",
        "properties": {
          "collected_date": {
            "type": "string",
            "format": "date-time",
            "description": "Day of the measurement, normalized to 00:00"
          },
          "metrics": {
            "$ref": "#/components/schemas/NormalizedMetrics"
          },
          "engagement_base": {
            "type": "string",
            "enum": [
              "reach",
              "impressions",
              "followers"
            ],
            "description": "What the engagement rate is divided by. Two rows with different bases are not comparable: state the base whenever you put them in the same table."
          }
        },
        "required": [
          "collected_date",
          "metrics"
        ]
      },
      "PublicationsPublicationsStatsList": {
        "type": "object",
        "properties": {
          "range": {
            "type": "object",
            "description": "The resolved range and the immediately preceding period of the same length, which is what `summary.previous_total` covers.",
            "properties": {
              "from_date": {
                "type": "string",
                "format": "date-time"
              },
              "to_date": {
                "type": "string",
                "format": "date-time"
              },
              "previous_from_date": {
                "type": "string",
                "format": "date-time"
              },
              "previous_to_date": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "metric": {
            "type": "string",
            "description": "Metric the listing is ordered by"
          },
          "summary": {
            "type": "object",
            "description": "Aggregates for the whole organization in the range. Omitted when `summary=false`.",
            "properties": {
              "total": {
                "$ref": "#/components/schemas/NormalizedMetrics"
              },
              "previous_total": {
                "$ref": "#/components/schemas/NormalizedMetrics"
              },
              "by_network": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "social_network": {
                      "type": "string"
                    },
                    "publications": {
                      "type": "integer"
                    },
                    "metrics": {
                      "$ref": "#/components/schemas/NormalizedMetrics"
                    }
                  }
                }
              }
            }
          },
          "publications": {
            "type": "array",
            "description": "The requested page. Each publication carries its last known `metrics`, `engagement_base` and `stats_updated_date`; a publication that has not been measured yet has none of them.",
            "items": {
              "$ref": "#/components/schemas/Publication"
            }
          },
          "total": {
            "type": "integer",
            "description": "Publications matching the filters, for paging"
          }
        }
      },
      "SocialCredentials": {
        "type": "object",
        "description": "What can be read back about an organization's own application. The secrets are not here and never will be.",
        "properties": {
          "client_id": {
            "type": "string"
          },
          "application_name": {
            "type": "string",
            "description": "The application's name, as Discord returned it when the credentials were validated."
          },
          "verified_date": {
            "type": "string",
            "format": "date-time",
            "description": "When the credentials were last validated against Discord."
          },
          "has_client_secret": {
            "type": "boolean",
            "description": "Whether a secret is stored. Never the secret."
          },
          "has_bot_token": {
            "type": "boolean",
            "description": "Whether a bot token is stored. Never the token."
          }
        }
      },
      "SocialIdentifier": {
        "type": "object",
        "description": "The same person, on one channel.",
        "properties": {
          "_id": {
            "type": "string"
          },
          "external_identifier": {
            "type": "string",
            "description": "Identifier of the contact on that channel."
          },
          "social_network": {
            "$ref": "#/components/schemas/ContactChannel"
          },
          "last_send_date": {
            "type": "string",
            "format": "date-time",
            "description": "Last message received from the contact. On WhatsApp it is what opens the 24-hour window in which a free-form message is allowed."
          }
        },
        "required": [
          "_id",
          "social_network"
        ]
      },
      "SocialNetwork": {
        "enum": [
          "facebook",
          "instagram",
          "linkedin",
          "tiktok",
          "twitter",
          "whatsapp",
          "youtube",
          "google_business",
          "bluesky",
          "discord"
        ],
        "type": "string",
        "description": "A social network supported by PlanVortex.\n\n**This list grows.** Treat it as an open enumeration: a client that rejects an unknown value breaks the day a network is added, which happens several times a year. Not every network does everything — ask `GET /social_capabilities`."
      },
      "StatsSettings": {
        "type": "object",
        "description": "Statistics collection settings. Only `auto_refresh_twitter` is read; any other key is ignored.",
        "properties": {
          "auto_refresh_twitter": {
            "type": "boolean",
            "default": true,
            "description": "Whether the background job refreshes X (Twitter) statistics automatically. Every other network is free and always refreshed; X charges 1 credit per read, so this is the one setting that makes the robot spend the client's credits. Anything other than an explicit `false` is treated as `true`."
          }
        }
      },
      "Success": {
        "type": "object",
        "description": "What an operation with nothing to return answers. A failure never looks like this: it comes back as an `Error` with HTTP 400.",
        "properties": {
          "success": {
            "type": "boolean"
          }
        },
        "required": [
          "success"
        ]
      },
      "Upload": {
        "type": "object",
        "description": "A file in the organization's library.\n\n**`public_path` is a signed, temporary URL, not a permanent link.** Do not store it: it expires. Ask for the upload again when you need it.",
        "required": [
          "_id",
          "id_organization",
          "name",
          "file_type",
          "file_format",
          "file_properties",
          "file_externals",
          "is_temporal",
          "public_path",
          "creation_date"
        ],
        "properties": {
          "_id": {
            "type": "string"
          },
          "id_organization": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "file_type": {
            "type": "string",
            "enum": [
              "video",
              "image"
            ]
          },
          "file_format": {
            "type": "string",
            "enum": [
              "mp4",
              "jpeg",
              "gif",
              "png",
              "jpg"
            ],
            "description": "The format of the stored bytes. `heic`/`heif` are accepted at the door — that is what an iPhone produces — but never stored: they are converted to JPEG while being ingested, so an upload never comes back with one."
          },
          "file_properties": {
            "$ref": "#/components/schemas/FileProperties"
          },
          "file_externals": {
            "type": "array",
            "description": "Where this file ended up on each network it has been published to. Some networks keep their own copy and give it an identifier that is reused instead of uploading the bytes again.",
            "items": {
              "type": "object",
              "required": [
                "social_network",
                "external_identifier",
                "external_url"
              ],
              "properties": {
                "social_network": {
                  "$ref": "#/components/schemas/SocialNetwork"
                },
                "external_identifier": {
                  "type": "string"
                },
                "external_url": {
                  "type": "string"
                }
              }
            }
          },
          "cover_image": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Upload"
              }
            ],
            "description": "Cover of a video, which is another upload of its own and comes back already resolved. It never appears on its own in the library listing."
          },
          "cover_offset": {
            "type": "integer",
            "description": "Point of the video, in milliseconds, used as the cover frame."
          },
          "is_temporal": {
            "type": "boolean",
            "description": "`true` on a file the platform created for itself — a crop made to fit a network's aspect ratio. Temporary files are not part of the library listing."
          },
          "public_path": {
            "type": "string",
            "format": "uri",
            "description": "Download URL. IMPORTANT: it is a TEMPORARY, signed URL, not a permanent link. It expires (24 hours by default) and must not be stored or shared: ask for the upload again to get a valid one. The URL stays byte-identical within the same hour, so it can be cached for that long.",
            "example": "https://a1b2c3d4e5f6.eu.r2.cloudflarestorage.com/planvortex-prod/orgs/60b.../60c.../foto.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=86400&X-Amz-Signature=..."
          },
          "creation_date": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "UploadsUploadList": {
        "type": "object",
        "properties": {
          "uploads": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Upload"
            }
          },
          "total": {
            "type": "integer"
          }
        },
        "required": [
          "uploads",
          "total"
        ]
      },
      "UploadsUploadOne": {
        "type": "object",
        "properties": {
          "upload": {
            "$ref": "#/components/schemas/Upload"
          }
        },
        "required": [
          "upload"
        ]
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "scheme": "bearer",
        "type": "http",
        "description": "Send `Authorization: Bearer <token>`. The token can be a Keycloak user JWT, an app token obtained through the client-credentials flow, or a temporal connect token. Which of the three identities an endpoint accepts is published per operation in `x-planvortex-identity`.",
        "bearerFormat": "JWT"
      },
      "clientCredentials": {
        "type": "oauth2",
        "description": "The identity an integration uses. Exchange the `client_id` and `client_secret` of a client app for an access token at `POST /oauth/token` and send it as `Authorization: Bearer <access_token>`.\n\nThe token endpoint is part of this API on purpose: an integration only ever configures `https://api.planvortex.com`, and the identity provider behind it is not part of the public contract. Credentials travel either in the body (`client_secret_post`) or in `Authorization: Basic` (`client_secret_basic`).\n\n**This is a server-side flow.** The `client_secret` cannot live in a browser bundle. To let an end user connect their own social account from a browser, issue a temporal connect token instead — `GET /organizations/{id_organization}/temporal_connect_token`.",
        "flows": {
          "clientCredentials": {
            "scopes": {},
            "tokenUrl": "https://api.planvortex.com/v1.0.0/oauth/token"
          }
        }
      }
    },
    "parameters": {
      "AiPlansidAiPlan": {
        "name": "id_ai_plan",
        "in": "path",
        "required": true,
        "description": "AI plan identifier",
        "schema": {
          "type": "string"
        }
      },
      "AiPlansidClient": {
        "name": "id_client",
        "in": "path",
        "required": true,
        "description": "Client identifier",
        "schema": {
          "type": "string"
        }
      },
      "AiPlansidOrganization": {
        "name": "id_organization",
        "in": "path",
        "required": true,
        "description": "Organization identifier",
        "schema": {
          "type": "string"
        }
      },
      "AppsidApp": {
        "name": "id_app",
        "in": "path",
        "description": "App identifier",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "AppsidClient": {
        "name": "id_client",
        "in": "path",
        "description": "Client identifier",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "Appslimit": {
        "name": "limit",
        "in": "query",
        "description": "Records per page (pagination)",
        "required": false,
        "schema": {
          "type": "integer"
        }
      },
      "Appsoffset": {
        "name": "offset",
        "in": "query",
        "description": "Records to skip (pagination)",
        "required": false,
        "schema": {
          "type": "integer",
          "default": 0
        }
      },
      "CommentsidAccount": {
        "name": "id_account",
        "in": "path",
        "required": true,
        "description": "Connected account identifier",
        "schema": {
          "type": "string"
        }
      },
      "CommentsidComment": {
        "name": "id_comment",
        "in": "path",
        "required": true,
        "description": "Comment identifier (PlanVortex's `_id`, not the network's `external_id`)",
        "schema": {
          "type": "string"
        }
      },
      "CommentsidOrganization": {
        "name": "id_organization",
        "in": "path",
        "required": true,
        "description": "Organization identifier",
        "schema": {
          "type": "string"
        }
      },
      "CommentsidPublication": {
        "name": "id_publication",
        "in": "path",
        "required": true,
        "description": "Publication identifier",
        "schema": {
          "type": "string"
        }
      },
      "CommentsthreadLimit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "How many to read from the network. **On X it is capped** regardless of what you send: a page there is a spending ceiling, not a pagination preference.",
        "schema": {
          "type": "integer",
          "default": 25
        }
      },
      "CommentsthreadOffset": {
        "name": "offset",
        "in": "query",
        "required": false,
        "description": "Page cursor. Pass back the `next_cursor` of the previous response — it is an opaque token from the network, not a numeric offset.",
        "schema": {
          "type": "string"
        }
      },
      "ContactsidContact": {
        "name": "id_contact",
        "in": "path",
        "description": "Contact identifier",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "ContactsidOrganization": {
        "name": "id_organization",
        "in": "path",
        "description": "Organization identifier",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "Contactslimit": {
        "name": "limit",
        "in": "query",
        "description": "Records per page (pagination)",
        "required": false,
        "schema": {
          "type": "integer",
          "default": 10
        }
      },
      "Contactsoffset": {
        "name": "offset",
        "in": "query",
        "description": "Records to skip (pagination)",
        "required": false,
        "schema": {
          "type": "integer",
          "default": 0
        }
      },
      "DashboardfromDate": {
        "name": "from_date",
        "in": "query",
        "description": "Start of the range, ISO 8601. Defaults to 30 days before `to_date`. A range longer than 366 days answers error 1003.",
        "required": false,
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      },
      "DashboardidOrganization": {
        "name": "id_organization",
        "in": "path",
        "description": "Organization identifier",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "DashboardtoDate": {
        "name": "to_date",
        "in": "query",
        "description": "End of the range, ISO 8601. Defaults to now.",
        "required": false,
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      },
      "IntegrationsidIntegration": {
        "name": "id_integration",
        "in": "path",
        "required": true,
        "description": "Integration identifier",
        "schema": {
          "type": "string"
        }
      },
      "IntegrationsidOrganization": {
        "name": "id_organization",
        "in": "path",
        "required": true,
        "description": "Organization identifier",
        "schema": {
          "type": "string"
        }
      },
      "MessagesfromDate": {
        "name": "from_date",
        "in": "query",
        "description": "Start of the range, ISO 8601. Defaults to the start of the current month.",
        "required": false,
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      },
      "MessagesgroupBy": {
        "name": "group_by",
        "in": "query",
        "description": "Group the count into a series. Omit it and the answer is a single `total` instead.",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "day",
            "month",
            "year"
          ]
        }
      },
      "MessagesidAccount": {
        "name": "id_account",
        "in": "path",
        "description": "Connected account identifier",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "MessagesidContact": {
        "name": "id_contact",
        "in": "path",
        "description": "Contact identifier",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "MessagesidOrganization": {
        "name": "id_organization",
        "in": "path",
        "description": "Organization identifier",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "Messageslimit": {
        "name": "limit",
        "in": "query",
        "description": "Records per page (pagination)",
        "required": false,
        "schema": {
          "type": "integer",
          "default": 10
        }
      },
      "Messagesoffset": {
        "name": "offset",
        "in": "query",
        "description": "Records to skip (pagination)",
        "required": false,
        "schema": {
          "type": "integer",
          "default": 0
        }
      },
      "MessagestoDate": {
        "name": "to_date",
        "in": "query",
        "description": "End of the range, ISO 8601. Defaults to the end of the current month.",
        "required": false,
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      },
      "OrganizationsRolesidOrganization": {
        "name": "id_organization",
        "in": "path",
        "description": "Organization identifier",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "OrganizationsRolesidRol": {
        "name": "id_rol",
        "in": "path",
        "description": "Role identifier",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "OrganizationsRolesidUser": {
        "name": "id_user",
        "in": "path",
        "description": "User identifier",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "OrganizationsRoleslimit": {
        "name": "limit",
        "in": "query",
        "description": "Maximum number of records returned (pagination)",
        "required": false,
        "schema": {
          "type": "integer",
          "default": 10
        }
      },
      "OrganizationsRolesoffset": {
        "name": "offset",
        "in": "query",
        "description": "Number of records to skip (pagination)",
        "required": false,
        "schema": {
          "type": "integer",
          "default": 0
        }
      },
      "ProductsidAccount": {
        "name": "id_account",
        "in": "path",
        "description": "Connected account identifier",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "ProductsidOrganization": {
        "name": "id_organization",
        "in": "path",
        "description": "Organization identifier",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "Productslimit": {
        "name": "limit",
        "in": "query",
        "description": "Records per page (pagination)",
        "required": false,
        "schema": {
          "type": "integer",
          "default": 10
        }
      },
      "Productsoffset": {
        "name": "offset",
        "in": "query",
        "description": "Records to skip (pagination)",
        "required": false,
        "schema": {
          "type": "integer",
          "default": 0
        }
      }
    },
    "responses": {
      "AppsError": {
        "description": "Error. Classify by `code` in the body, never by the HTTP status.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "CommentsCommentError": {
        "description": "The request failed. The body carries the PlanVortex error code in `code`:\n\n| Code | Meaning |\n| --- | --- |\n| `516` | Comments require a paid plan. Every endpoint here returns this on the free plan. |\n| `700` | The account is not connected — no valid token, or nothing to ask the network about. |\n| `936` | The publication was never sent, so it has no thread to read. |\n| `940` | The client's monthly X credits are exhausted. Only X. |\n| `945` | This network has no comments (TikTok, WhatsApp). Carries `social_network` in `data`. |\n| `946` | The network has comments but does not allow **this action**: hiding on LinkedIn or Google Business, deleting someone else's on Instagram or X, deleting a review. Carries `social_network` and `action` in `data`. See `GET /social_comment_actions`. |\n| `947` | The comment no longer exists on the network. What is stored is a photograph; the network always wins. Carries `external_id` in `data`. |\n| `948` | The reply is empty or longer than the network allows. Carries the limit in `data.max`; the same number is published in `comment_characters` of `GET /social_limits`. |\n| `951` | The Google Business listing is not verified, so it cannot reply to its reviews. It is the state of the customer's profile, not of your token. |\n| `952` | PlanVortex's Google Cloud project has no approved access to the Google Business API yet. Until it does, reviews cannot be read or replied to. |\n| `1101` | Invalid organization. |\n| `1501` | The comment's account could not be resolved. |",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ContactsError": {
        "description": "Error. Classify by `code` in the body, never by the HTTP status.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "DashboardError": {
        "description": "Error. Classify by `code` in the body, never by the HTTP status.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "MessagesError": {
        "description": "Error. Classify by `code` in the body, never by the HTTP status.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ProductsError": {
        "description": "Error. Classify by `code` in the body, never by the HTTP status.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "x-planvortex-bundle": {
    "description": "GENERADO. Los documentos de PlanVortexHome/swagger unidos en uno, que es lo que necesita un generador de codigo. No se edita a mano: se edita el spec de PlanVortexHome y se ejecuta `npm run generate`.",
    "sources": [
      "accounts-swagger.json",
      "ai_plans-swagger.json",
      "apps-swagger.json",
      "catalog-swagger.json",
      "clients-swagger.json",
      "clients_roles-swagger.json",
      "comments-swagger.json",
      "contacts-swagger.json",
      "dashboard-swagger.json",
      "integrations-swagger.json",
      "messages-swagger.json",
      "organizations-swagger.json",
      "organizations_roles-swagger.json",
      "products-swagger.json",
      "publications-swagger.json",
      "uploads-swagger.json"
    ]
  }
}
