{
  "openapi": "3.1.0",
  "info": {
    "title": "Revnu API",
    "version": "2026-09-29",
    "summary": "Read and approve your Revnu growth agent's review queue, message it, and pull results.",
    "description": "Revnu runs GTM on autopilot for software businesses. This API is the agent's front door: nothing sends, publishes or spends except through the same approvals the dashboard uses. Full docs: https://revnu.com/docs (markdown index at https://revnu.com/docs/llms.txt).",
    "termsOfService": "https://revnu.com/terms",
    "contact": {
      "name": "Revnu",
      "url": "https://revnu.com/contact"
    }
  },
  "externalDocs": {
    "description": "Revnu Docs",
    "url": "https://revnu.com/docs"
  },
  "servers": [
    {
      "url": "https://revnu.com"
    }
  ],
  "tags": [
    {
      "name": "API Reference",
      "description": "The personal-token API at /api/v1: review queue, agent, results, workflows, leads and the block list."
    },
    {
      "name": "Content API",
      "description": "Read-only JSON for the posts Revnu publishes, for rendering a blog on your own site."
    },
    {
      "name": "Measurement",
      "description": "Server-side conversion events, tied back to the visit that brought the customer."
    }
  ],
  "paths": {
    "/api/v1/me": {
      "get": {
        "operationId": "me",
        "summary": "Get the current token",
        "description": "Who this token is: label, scopes, the account it belongs to, and the rate limits it is held to. Needs no scope.\n\n**Response.** ```json\n{\n  \"me\": {\n    \"tokenId\": \"…\",\n    \"label\": \"wall dashboard\",\n    \"scopes\": [\"review:read\", \"review:write\"],\n    \"createdAt\": 1789837627802,\n    \"lastUsedAt\": 1789851456304,\n    \"account\": { \"id\": \"…\", \"name\": \"Acme\" },\n    \"limits\": {\n      \"reads\": { \"perMinute\": 300, \"burst\": 600 },\n      \"writes\": { \"perMinute\": 60, \"burst\": 120 },\n      \"messages\": { \"perMinute\": 30, \"burst\": 60 },\n      \"account\": { \"perMinute\": 900, \"burst\": 1800 }\n    }\n  }\n}\n```\n\n`401` for an unknown or revoked token.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/me"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "``json { \"me\": { \"tokenId\": \"…\", \"label\": \"wall dashboard\", \"scopes\": [\"review:read\", \"review:write\"], \"createdAt\": 1789837627802, \"lastUsedAt\": 1789851456304, \"account\": { \"id\": \"…\", \"name\": \"Acme\" }, \"limits\": { \"reads\": { \"perMinute\": 300, \"burst\": 600 }, \"writes\": { \"perMinute\": 60, \"burst\": 120",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/review": {
      "get": {
        "operationId": "reviewList",
        "summary": "List the review queue",
        "description": "What is waiting for the customer's approval, each card carrying the exact verbs the API accepts for it.\n\nRequires the `review:read` scope.\n\n**Response.** `{ \"items\": [card, …] }`, newest first within priority band. A card:\n\n```json\n{\n  \"id\": \"k97…\",\n  \"kind\": \"approval\",\n  \"sourceType\": \"gmail_draft\",\n  \"status\": \"pending\",\n  \"title\": \"Reply to Priya at Acme\",\n  \"prompt\": \"…\",\n  \"summary\": \"…\",\n  \"detail\": null,\n  \"priority\": \"high\",\n  \"options\": null,\n  \"textInput\": null,\n  \"budgetChange\": null,\n  \"payload\": { \"…\": \"the draft, the research; long text capped at 8 KB\" },\n  \"actions\": [\"send\", \"snooze\", \"dismiss\"],\n  \"primaryLabel\": \"Send Reply\",\n  \"autoResolveAt\": 1758326400000,\n  \"dueAt\": null, \"snoozedUntil\": null,\n  \"leadId\": \"…\", \"leadContactId\": null, \"postId\": null,\n  \"createdAt\": 1758240000000, \"updatedAt\": 1758240000000, \"resolvedAt\": null, \"resolution\": null\n}\n```\n\nDrive your UI from `actions` and `primaryLabel`. See [Build a Review Screen](/docs/build-a-review-screen).",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/review-list"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "review:read",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "pending or needs_revision. Default: both.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "1 to 100. Default 50.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"items\": [card, …] }, newest first within priority band. A card: ``json { \"id\": \"k97…\", \"kind\": \"approval\", \"sourceType\": \"gmail_draft\", \"status\": \"pending\", \"title\": \"Reply to Priya at Acme\", \"prompt\": \"…\", \"summary\": \"…\", \"detail\": null, \"priority\": \"high\", \"options\": null, \"textInput\": null, \"b",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/review/{id}": {
      "get": {
        "operationId": "reviewGet",
        "summary": "Get one card",
        "description": "One review card by id, any status, including its full payload.\n\nRequires the `review:read` scope.\n\n**Response.** `{ \"item\": card }` with the same shape as [the list](/docs/review-list). `404` for an unknown id or another account's id; the two are indistinguishable on purpose.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/review-get"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "review:read",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"item\": card } with the same shape as the list. 404 for an unknown id or another account's id; the two are indistinguishable on purpose.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/review/{id}/answer": {
      "post": {
        "operationId": "reviewAnswer",
        "summary": "Answer a card",
        "description": "Resolve a card the way the dashboard's primary button would: approve, answer a question, or choose an option.\n\nRequires the `review:write` scope.\n\n**Response.** | Code | Meaning |\n|---|---|\n| 200 | `{ ok, kind, result? }` |\n| 409 | `invalid_action` with the current `actions`, a guard rejection with a `message`, or `use_send` for a Gmail card |",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/review-answer"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "review:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "| Field | Type | Meaning |\n|---|---|---|\n| `action` | `approve`, `answer` or `choose` | Must be in the card's `actions`. |\n| `text` | string, up to 8,000 UTF-8 bytes | For `answer`, and optionally with `approve`. |\n| `optionValues` | string[] | For `choose`: exactly one of the card's option values. |",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok, kind, result? }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "invalid_action with the current actions, a guard rejection with a message, or use_send for a Gmail card",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/review/{id}/send": {
      "post": {
        "operationId": "reviewSend",
        "summary": "Send a Gmail card",
        "description": "Approve and send a Gmail reply or draft, optionally with an edited body. This sends real email.\n\nRequires the `review:write` scope.\n\n**Response.** `200 { ok, … }` once the provider accepted it. `409` if the card was already handled or is not a Gmail card. `503 delivery_pending` with a `Retry-After` header if the provider's answer was lost: the send is fenced for reconciliation rather than risked twice, so wait and retry the same call. `503 unavailable`, also with `Retry-After`, when the delivery service could not be reached.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/review-send"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "review:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "| Field | Meaning |\n|---|---|\n| `body` | Optional, Gmail replies only. Send this text instead of the draft. Cannot be empty if present. A `body` on a `gmail_draft` card is a `400`: a draft always sends the copy that was reviewed. |",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "200 { ok, … } once the provider accepted it. 409 if the card was already handled or is not a Gmail card. 503 delivery_pending with a Retry-After header if the provider's answer was lost: the send is fenced for reconciliation rather than risked twice, so wait and retry the same call. 503 unavailable,",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/review/{id}/request-changes": {
      "post": {
        "operationId": "reviewRequestChanges",
        "summary": "Request changes",
        "description": "Send a card back to the agent with feedback. A revised version returns to the queue.\n\nRequires the `review:write` scope.\n\n**Response.** `200 { ok: true }`. `409 invalid_action` if the card no longer accepts it.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/review-request-changes"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "review:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "`text`, required, up to 8,000 UTF-8 bytes. The agent starts revising about 30 seconds later; the card sits in `needs_revision` until the new version comes back for review.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "200 { ok: true }. 409 invalid_action if the card no longer accepts it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/review/{id}/snooze": {
      "post": {
        "operationId": "reviewSnooze",
        "summary": "Snooze a card",
        "description": "Hide a card until a time. Cancels any auto-ship deadline on it.\n\nRequires the `review:write` scope.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/review-snooze"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "review:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "`until`, epoch milliseconds, in the future. Snoozing cancels the trust ladder's `autoResolveAt` so nothing ships while you are not looking.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/review/{id}/dismiss": {
      "post": {
        "operationId": "reviewDismiss",
        "summary": "Dismiss a card",
        "description": "Decline a card. It closes without shipping, the agent's trust for that kind of work resets, and the reason teaches it.\n\nRequires the `review:write` scope.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/review-dismiss"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "review:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "`reason`, optional, up to 2,000 characters. Outbound-setup cards also cancel their proposal.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/agent/messages": {
      "post": {
        "operationId": "agentMessages",
        "summary": "Send a message",
        "description": "Message your agent exactly as the dashboard chat does, and optionally hold the request open for the reply.\n\nRequires the `agent:message` scope.\n\n**Response.** | Code | Meaning |\n|---|---|\n| 202 | `{ requestId, deduplicated, notSent?, message? }`. Queued; poll [the message](/docs/agent-message-status). With `waitSeconds`, `message` is the last state seen, and `waitEnded` (`unauthorized` or `poll_failed`) says when the wait stopped early. An `unauthorized` stop carries no `message`. |\n| 200 | Only with `waitSeconds`: `message` is terminal and `replies` is filled. |\n| 400 | Missing or empty `text`, `text` over 8,000 bytes, a non-string client id, a non-boolean `interrupt` or `queue`, `queue` together with `interrupt`, or a `waitSeconds` outside 0 to 120. |\n| 409 | `text` over 4,000 characters, or a client id with characters outside `A-Za-z0-9_.:-`. |\n\nWith `interrupt: false` and an unfinished earlier message, either code can carry `notSent: true`: `requestId` and `message` are then the earlier request's, and nothing new was sent.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/agent-messages"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "agent:message",
        "requestBody": {
          "required": true,
          "description": "| Field | Type | Meaning |\n|---|---|---|\n| `text` | string, required | Up to 4,000 characters. |\n| `clientRequestId` | string | Up to 128 characters of `A-Za-z0-9_.:-`. A retry with the same id returns the original `requestId`. Never reuse an id after a `failed` message; send again with a new one. |\n| `waitSeconds` | integer, 0 to 120 | Hold the request until the agent has replied and finished the turn, or failed. |\n| `interrupt` | boolean, default `true` | A new message stops the agent's running turn. Send `false` to leave unfinished work alone: while this token's newest message is still queued or running, nothing is sent and the response names that request with `notSent: true`. |\n| `queue` | boolean, default `false` | Send `true` to hand over another job without stopping the running one: the message is sent and the agent starts it once its current work is done. Use it when your software sends several jobs in a row, such as one list of companies per market. It stays `queued` for up to six hours while earlier work runs, then counts as `failed`. Cannot be combined with `interrupt`. |",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Only with waitSeconds: message is terminal and replies is filled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "202": {
            "description": "{ requestId, deduplicated, notSent?, message? }. Queued; poll the message. With waitSeconds, message is the last state seen, and waitEnded (unauthorized or poll_failed) says when the wait stopped early. An unauthorized stop carries no message.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Missing or empty text, text over 8,000 bytes, a non-string client id, a non-boolean interrupt or queue, queue together with interrupt, or a waitSeconds outside 0 to 120.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "text over 4,000 characters, or a client id with characters outside A-Za-z0-9_.:-.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/agent/messages/{requestId}": {
      "get": {
        "operationId": "agentMessageStatus",
        "summary": "Poll a message",
        "description": "One message's status and every reply the agent has sent since it landed.\n\nRequires the `agent:message` scope.\n\n**Response.** ```json\n{\n  \"message\": {\n    \"requestId\": \"f2a283fd-…\",\n    \"status\": \"replied\",\n    \"text\": \"What did we ship this week?\",\n    \"sentAt\": 1789851456304,\n    \"replies\": [{ \"text\": \"Three things shipped: …\", \"at\": 1789851468121 }]\n  }\n}\n```\n\n`status` is `queued`, `running`, `replied` or `failed` (with a `failure` message; re-send with a new client id). `replied` means the agent answered and finished the turn. The agent may answer in more than one bubble, and it often posts progress while it works, so `replies` can fill while the status is still `running`. It is a list, oldest first, holding the latest five bubbles. `interrupted: true` means a later message stopped this turn before it finished. `404` for an unknown id, a colleague's dashboard turn, or another account's message.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/agent-message-status"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "agent:message",
        "parameters": [
          {
            "name": "requestId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "``json { \"message\": { \"requestId\": \"f2a283fd-…\", \"status\": \"replied\", \"text\": \"What did we ship this week?\", \"sentAt\": 1789851456304, \"replies\": [{ \"text\": \"Three things shipped: …\", \"at\": 1789851468121 }] } } ` status is queued, running, replied or failed (with a failure message; re-send with a new",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/agent/thread": {
      "get": {
        "operationId": "agentThread",
        "summary": "Read the thread",
        "description": "This token's conversation with the agent, newest first.\n\nRequires the `agent:message` scope.\n\n**Response.** `{ \"items\": [{ role, requestId, text, at }, …] }` with `role` of `user` or `agent`. Each token is its own thread; dashboard and Slack conversations are not included.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/agent-thread"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "agent:message",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "1 to 100. Default 50.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"items\": [{ role, requestId, text, at }, …] } with role of user or agent. Each token is its own thread; dashboard and Slack conversations are not included.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/agent/status": {
      "get": {
        "operationId": "agentStatus",
        "summary": "Get agent status",
        "description": "Whether the agent is up and busy: sandbox state, last activity, and the next scheduled wake.\n\nRequires the `results:read` scope.\n\n**Response.** ```json\n{ \"status\": { \"sandbox\": \"alive\", \"running\": false, \"lastActivityAt\": 1789832474865, \"lastTurnAt\": 1789832490530, \"pendingWakes\": 2, \"nextWakeAt\": 1790002800000 } }\n```\n\n`running` is derived: a turn that started in the last 30 minutes and has not finished. A `dead` sandbox is normal between turns; the next message or wake spawns one.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/agent-status"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "results:read",
        "responses": {
          "200": {
            "description": "``json { \"status\": { \"sandbox\": \"alive\", \"running\": false, \"lastActivityAt\": 1789832474865, \"lastTurnAt\": 1789832490530, \"pendingWakes\": 2, \"nextWakeAt\": 1790002800000 } } ` running is derived: a turn that started in the last 30 minutes and has not finished. A dead` sandbox is normal between turns; ",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/results": {
      "get": {
        "operationId": "results",
        "summary": "Get results",
        "description": "The growth scoreboard for a window ending now: per-channel sections that are either available with data or unavailable with a reason.\n\nRequires the `results:read` scope.\n\n**Response.** `{ \"results\": { window, baseline, email, ads, seo, social, aeo, linkedin, harness, … } }`. Each channel section is `{ \"available\": true, \"data\": {…} }` or `{ \"available\": false, \"reason\": \"…\" }`. Zero and unknown are different numbers.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/results"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "results:read",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "Start of the window, epoch milliseconds, clamped to between seven days and one hour ago. Default 24 hours ago.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"results\": { window, baseline, email, ads, seo, social, aeo, linkedin, harness, … } }. Each channel section is { \"available\": true, \"data\": {…} } or { \"available\": false, \"reason\": \"…\" }. Zero and unknown are different numbers.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/workflows": {
      "get": {
        "operationId": "workflows",
        "summary": "List workflows",
        "description": "The account's recurring workflows with their schedule, next run and step list.\n\nRequires the `results:read` scope.\n\n**Response.** `{ \"workflows\": [{ workflowId, name, description, status, scheduleText, scheduleEcho, scheduleError, deliverTo, nextRunAt, version, steps: [{ id, title, details, gate? }], url }, …] }`.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/workflows"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "results:read",
        "responses": {
          "200": {
            "description": "{ \"workflows\": [{ workflowId, name, description, status, scheduleText, scheduleEcho, scheduleError, deliverTo, nextRunAt, version, steps: [{ id, title, details, gate? }], url }, …] }.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      },
      "post": {
        "operationId": "workflowCreate",
        "summary": "Create a workflow",
        "description": "Compose a recurring workflow from a schedule in plain words and a list of steps.\n\nRequires the `workflows:write` scope.\n\n**Response.** `{ \"workflow\": { workflowId, status, version, scheduleText, scheduleEcho, scheduleError, nextRunAt, steps, url, … } }`, status `201`. The workflow is live. The schedule resolves after the write: read it back for `scheduleEcho` and `nextRunAt`, or `scheduleError` when the words could not be understood.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/workflow-create"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "workflows:write",
        "requestBody": {
          "required": true,
          "description": "`name` (up to 120 characters), `scheduleText` (plain words, up to 200 characters), `steps` (1 to 25). A step is `{ title, details?, gate?, id? }`: `details` is up to 8 lines, `gate: { on: true, label? }` means the step waits for a human ok before it acts. Optional `description` (240 characters) and `deliverTo`, one of the account's connected destinations. Unknown fields are a `400`.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{ \"workflow\": { workflowId, status, version, scheduleText, scheduleEcho, scheduleError, nextRunAt, steps, url, … } }, status 201. The workflow is live. The schedule resolves after the write: read it back for scheduleEcho and nextRunAt, or scheduleError when the words could not be understood.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/workflows/{id}/runs": {
      "get": {
        "operationId": "workflowRuns",
        "summary": "List workflow runs",
        "description": "Run history for one workflow, newest first, with per-step outcomes.\n\nRequires the `results:read` scope.\n\n**Response.** `{ \"runs\": [run, …] }`, newest first, each with its per-step outcomes. `404` for an unknown or another account's workflow.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/workflow-runs"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "results:read",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "1 to 50. Default 10.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"runs\": [run, …] }, newest first, each with its per-step outcomes. 404 for an unknown or another account's workflow.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/workflows/{id}": {
      "patch": {
        "operationId": "workflowUpdate",
        "summary": "Update a workflow",
        "description": "Change a workflow's name, schedule, steps, destination or status. Steps are replaced whole.\n\nRequires the `workflows:write` scope.\n\n**Response.** `{ \"workflow\": { … } }`, the updated workflow with its new `version`. `404` for an unknown or another account's workflow. `409` when `deliverTo` is not a connected destination; the message lists the options.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/workflow-update"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "workflows:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "Any of `name`, `description`, `scheduleText`, `steps`, `deliverTo`, `status` (`\"live\"` or `\"paused\"`), with the same limits as create. `steps` replaces the whole list, so send back the steps you are not changing and keep their `id` so run history stays attached. A changed `scheduleText` is re-resolved after the write.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ \"workflow\": { … } }, the updated workflow with its new version. 404 for an unknown or another account's workflow. 409 when deliverTo is not a connected destination; the message lists the options.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      },
      "delete": {
        "operationId": "workflowDelete",
        "summary": "Delete a workflow",
        "description": "Remove a workflow. Nothing fires again, and it drops out of every read.\n\nRequires the `workflows:write` scope.\n\n**Response.** `{ \"ok\": true, \"name\": \"…\" }`. The workflow disappears from `GET /workflows`, its pending wake is cancelled, and `GET /workflows/{id}/runs` answers `404` from then on. `404` for an unknown or another account's workflow.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/workflow-delete"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "workflows:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"ok\": true, \"name\": \"…\" }. The workflow disappears from GET /workflows, its pending wake is cancelled, and GET /workflows/{id}/runs answers 404 from then on. 404 for an unknown or another account's workflow.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/workflows/{id}/pause": {
      "post": {
        "operationId": "workflowPause",
        "summary": "Pause a workflow",
        "description": "Stop a workflow firing without losing its steps or history.\n\nRequires the `workflows:write` scope.\n\n**Response.** `{ \"workflow\": { …, status: \"paused\" } }`. The pending wake is cancelled; nothing fires until it is resumed. Idempotent. `404` for an unknown or another account's workflow.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/workflow-pause"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "workflows:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"workflow\": { …, status: \"paused\" } }. The pending wake is cancelled; nothing fires until it is resumed. Idempotent. 404 for an unknown or another account's workflow.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/workflows/{id}/resume": {
      "post": {
        "operationId": "workflowResume",
        "summary": "Resume a workflow",
        "description": "Set a paused workflow live again; the next run is scheduled from its cadence.\n\nRequires the `workflows:write` scope.\n\n**Response.** `{ \"workflow\": { …, status: \"live\", nextRunAt } }`. Idempotent. Resuming a workflow the agent proposed accepts it. `404` for an unknown or another account's workflow.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/workflow-resume"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "workflows:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"workflow\": { …, status: \"live\", nextRunAt } }. Idempotent. Resuming a workflow the agent proposed accepts it. 404 for an unknown or another account's workflow.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/leads": {
      "get": {
        "operationId": "leads",
        "summary": "List leads",
        "description": "Leads newest first with status, score, primary contact and your own columns, paged by an opaque cursor. Draft bodies are only on GET /leads/{id}.\n\nRequires the `leads:read` scope.\n\n**Response.** `{ \"leads\": [lead, …], \"nextCursor\": \"…\" | null }`. A page is bounded in rows and in bytes, so pass `nextCursor` back until it is null.\n\nA lead is `{ id, domain, companyName, status, replyKind, declineReason, score, scoreReasons, vertical, archetype, tags, fields, contact, createdAt }`. `fields` is your account's configured lead columns, exactly the dashboard table, as `[{ key, value }]` entries with each value cut at 1,000 characters, for example `[{ \"key\": \"account_case\", \"value\": \"…\" }, { \"key\": \"slack_thread\", \"value\": \"1790064680.268899\" }]`. The list never carries the draft or the research behind a value; read `GET /leads/{id}` for those.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/leads"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "leads:read",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filter by lead status.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "1 to 100.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The nextCursor from the previous page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"leads\": [lead, …], \"nextCursor\": \"…\" | null }. A page is bounded in rows and in bytes, so pass nextCursor back until it is null. A lead is { id, domain, companyName, status, replyKind, declineReason, score, scoreReasons, vertical, archetype, tags, fields, contact, createdAt }. fields is your acco",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/leads/{id}": {
      "get": {
        "operationId": "lead",
        "summary": "Get a lead",
        "description": "One lead with its contacts, the surfaced draft, and the research behind each column.\n\nRequires the `leads:read` scope.\n\n**Response.** `{ \"lead\": { …, fields, draft, research, contacts: [contact, …], contactsTruncated: false } }`. Contacts are capped at 50, with `contactsTruncated: true` when there were more. `404` for an unknown or another account's lead.\n\n`draft` is the surfaced draft, `{ subject, body }` (each cut at 16,000 characters), or `null` when there is none. `research` is a list of entries, one per key the agent saved research under: `{ key, reasoning, sources: [{ url, title }], sourcesTruncated }`, the agent's reasoning (cut at 8,000 characters) and up to 20 sources behind that value. A source whose URL is longer than 2,048 characters is left out rather than cut, and `sourcesTruncated` says when anything was left out. Keys with no visible column appear here too.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/lead"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "leads:read",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"lead\": { …, fields, draft, research, contacts: [contact, …], contactsTruncated: false } }. Contacts are capped at 50, with contactsTruncated: true when there were more. 404 for an unknown or another account's lead. draft is the surfaced draft, { subject, body } (each cut at 16,000 characters), or",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      },
      "patch": {
        "operationId": "leadUpdate",
        "summary": "Update a lead's labels",
        "description": "Set vertical, archetype and tags on a lead. The one write on a lead.\n\nRequires the `leads:write` scope.\n\n**Response.** `{ \"lead\": { id, domain, vertical, archetype, tags } }`, the labels as stored. The response carries nothing a `leads:read` token would show; read the lead back for the rest. Status, drafts, contacts and research stay read-only; the agent and the dashboard own them. `404` for an unknown or another account's lead.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/lead-update"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "leads:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "Any of `vertical`, `archetype` (strings, at most 120 characters) and `tags` (up to 20 strings of at most 64 characters, deduplicated, order kept). `null` clears a field. Unknown fields are a `400`, so a typo never silently does nothing.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ \"lead\": { id, domain, vertical, archetype, tags } }, the labels as stored. The response carries nothing a leads:read token would show; read the lead back for the rest. Status, drafts, contacts and research stay read-only; the agent and the dashboard own them. 404 for an unknown or another account'",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/blocklist": {
      "get": {
        "operationId": "blocklist",
        "summary": "List the block list",
        "description": "The do-not-contact entries the send and research gates enforce, newest first.\n\nRequires the `leads:read` scope.\n\n**Response.** `{ \"entries\": [entry, …], \"nextCursor\": \"…\" | null }`. An entry is `{ id, kind, value, reason, source, addedBy, addedAt }`. `kind` is `domain`, `email` or `linkedin`; a `linkedin` value is the profile URL in the form `https://www.linkedin.com/in/<slug>`; `source` says who added it: `customer_import`, `customer` (dashboard), `agent`, `opt_out_reply` (a provider-reported unsubscribe) or `api`.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/blocklist"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "leads:read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows per page, 1 to 200. Default 50.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The nextCursor from the previous page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"entries\": [entry, …], \"nextCursor\": \"…\" | null }. An entry is { id, kind, value, reason, source, addedBy, addedAt }. kind is domain, email or linkedin; a linkedin value is the profile URL in the form https://www.linkedin.com/in/<slug>; source says who added it: customer_import, customer (dashboar",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      },
      "post": {
        "operationId": "blocklistAdd",
        "summary": "Add block list entries",
        "description": "Block companies, mailboxes or LinkedIn profiles from outreach. Idempotent, up to 500 per call.\n\nRequires the `leads:write` scope.\n\n**Response.** `{ \"added\": 3, \"existing\": 0, \"invalid\": [] }`. `existing` counts values already on the list, so re-sending a whole list is safe. `invalid` lists the strings that could not be read as a domain, an email or a LinkedIn profile, trimmed and cut to 200 characters, blanks dropped; nothing else in the call is affected by them. Entries carry `source: \"api\"` and the token's label.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/blocklist-add"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "leads:write",
        "requestBody": {
          "required": true,
          "description": "`values`, 1 to 500 raw strings of up to 320 characters each: bare domains, emails, `mailto:` links, `Name <email>` forms or LinkedIn profile links (`linkedin.com/in/<slug>`, with or without `https://www.`), all normalised. Any other LinkedIn link, such as a company page, is invalid. `reason` is an optional note of up to 500 characters stored on every entry in the call.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ \"added\": 3, \"existing\": 0, \"invalid\": [] }. existing counts values already on the list, so re-sending a whole list is safe. invalid lists the strings that could not be read as a domain, an email or a LinkedIn profile, trimmed and cut to 200 characters, blanks dropped; nothing else in the call is a",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/blocklist/{id}": {
      "delete": {
        "operationId": "blocklistRemove",
        "summary": "Remove a block list entry",
        "description": "Unblock one company or mailbox by the id the list returns.\n\nRequires the `leads:write` scope.\n\n**Response.** `{ \"removed\": entry }`. `404` for an unknown or another account's id. `409` for an address that unsubscribed (`source: opt_out_reply`): it stays blocked.",
        "tags": [
          "API Reference"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/blocklist-remove"
        },
        "security": [
          {
            "personalToken": []
          }
        ],
        "x-revnu-scope": "leads:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"removed\": entry }. 404 for an unknown or another account's id. 409 for an address that unsubscribed (source: opt_out_reply): it stays blocked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/content/v1/posts": {
      "get": {
        "operationId": "contentPosts",
        "summary": "List posts",
        "description": "Every published post, newest first, with cursor paging and filters by content type and time.\n\n**Response.** `{ \"blog\": {…}, \"nav\": {…}, \"posts\": [summary, …], \"cursor\": \"…\" | null }`. Each summary carries `slug`, `title`, `metaDescription`, `canonicalPath`, `canonicalUrl`, `publishedAt` and `updatedAt`, which is everything a listing page or a sitemap needs.\n\nA post that disappears from the list was removed; drop the page.\n\n`400` for an unrecognised `type` or a non-numeric `since` or `limit`.",
        "tags": [
          "Content API"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/content-posts"
        },
        "security": [
          {
            "contentToken": []
          }
        ],
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Filter by content type, e.g. how_to.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "Epoch milliseconds: only posts published or updated after this.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Up to 200.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The cursor from the previous page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ \"blog\": {…}, \"nav\": {…}, \"posts\": [summary, …], \"cursor\": \"…\" | null }. Each summary carries slug, title, metaDescription, canonicalPath, canonicalUrl, publishedAt and updatedAt, which is everything a listing page or a sitemap needs. A post that disappears from the list was removed; drop the page.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "description": "missing or malformed Authorization header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "unknown or revoked token, or unknown slug; indistinguishable on purpose",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "slow down; Retry-After is set. A full site build fits comfortably.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/content/v1/posts/{slug}": {
      "get": {
        "operationId": "contentPost",
        "summary": "Get a post",
        "description": "One post, render-ready: markdown and safe HTML, table of contents, head metadata, JSON-LD and related posts.\n\n**Response.** ```json\n{\n  \"blog\": { \"creatorName\": \"…\", \"authorName\": \"…\", \"logoUrl\": \"…\", \"baseUrl\": \"…\" },\n  \"post\": {\n    \"slug\": \"graph-rag\",\n    \"title\": \"…\",\n    \"canonicalPath\": \"/guides/graph-rag\",\n    \"canonicalUrl\": \"https://yourdomain.com/blog/guides/graph-rag\",\n    \"body\": { \"markdown\": \"…\", \"html\": \"…\" },\n    \"toc\": [{ \"text\": \"…\", \"slug\": \"…\" }],\n    \"heroImageUrl\": \"https://…\",\n    \"faqItems\": [{ \"question\": \"…\", \"answer\": \"…\" }],\n    \"citations\": [\"https://…\"],\n    \"publishedAt\": 1753000000000,\n    \"updatedAt\": 1753100000000\n  },\n  \"headMeta\": { \"title\": \"…\", \"description\": \"…\", \"canonical\": \"…\", \"openGraph\": {}, \"twitter\": {} },\n  \"jsonLd\": [{ \"@type\": \"Article\" }, { \"@type\": \"BreadcrumbList\" }, { \"@type\": \"FAQPage\" }],\n  \"related\": [{ \"slug\": \"…\", \"title\": \"…\", \"canonicalPath\": \"…\" }]\n}\n```",
        "tags": [
          "Content API"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/content-post"
        },
        "security": [
          {
            "contentToken": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "``json { \"blog\": { \"creatorName\": \"…\", \"authorName\": \"…\", \"logoUrl\": \"…\", \"baseUrl\": \"…\" }, \"post\": { \"slug\": \"graph-rag\", \"title\": \"…\", \"canonicalPath\": \"/guides/graph-rag\", \"canonicalUrl\": \"https://yourdomain.com/blog/guides/graph-rag\", \"body\": { \"markdown\": \"…\", \"html\": \"…\" }, \"toc\": [{ \"text\": \"",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "description": "missing or malformed Authorization header",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "unknown or revoked token, or unknown slug; indistinguishable on purpose",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "slow down; Retry-After is set. A full site build fits comfortably.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/measurement/events": {
      "post": {
        "operationId": "eventApi",
        "summary": "Send Events",
        "description": "Tell Revnu from your server when a visitor signs up, books a call or pays, so the result is tied back to where they came from.\n\n**Response.** ```json\n{\n  \"accepted\": true,\n  \"matched\": false,\n  \"duplicate\": false,\n  \"enriched\": false,\n  \"event_id\": \"signup_8412\",\n  \"event_name\": \"signup\",\n  \"roles\": [],\n  \"attribution_status\": \"pending\"\n}\n```\n\n| Code | Meaning |\n|---|---|\n| 200 | Recorded and tied to one of your Revnu ad campaigns, or a duplicate of an event already recorded |\n| 202 | Recorded. Nothing to fix; most events land here |\n| 401 | Missing or invalid event key |\n| 409 | The same `event_id` was sent before with different values |\n| 413 | Body over 64 KB |\n| 422 | A field failed validation; `error` says which |\n| 429 | Rate limited; retry later with the same `event_id` |",
        "tags": [
          "Measurement"
        ],
        "externalDocs": {
          "url": "https://revnu.com/docs/event-api"
        },
        "security": [
          {
            "eventKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "description": "| Field | Required | Notes |\n|---|---|---|\n| `event_id` | yes | Your stable id for this event. 1-128 letters, digits, `.`, `_`, `:` or `-`. Sending the same id again is safe: it is reported as a duplicate, not counted twice. |\n| `event_name` | yes | Lowercase snake_case, for example `signup`, `demo_booked`, `purchase`. |\n| `occurred_at` | no | ISO 8601 date or milliseconds. Defaults to when Revnu receives it. |\n| `anonymous_id` | no | `window.revnu.anonymousId` from the visit. |\n| `email`, `phone`, `external_user_id` | no | Normalized and hashed for matching. |\n| `amount`, `currency` | no | Send both, for example `49` and `\"USD\"`. |\n| `properties` | no | An object of extra facts, for example `{ \"plan\": \"pro\" }`. |\n| `reversal_of` | no | The `event_id` of an earlier event this one cancels, such as a refund. Repeat the original `amount` and `currency`. |",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recorded and tied to one of your Revnu ad campaigns, or a duplicate of an event already recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "202": {
            "description": "Recorded. Nothing to fix; most events land here",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "description": "Missing or invalid event key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The same event_id was sent before with different values",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Body over 64 KB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "A field failed validation; error says which",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited; retry later with the same event_id",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "personalToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "rvp_…",
        "description": "Personal API token from Settings → API Tokens, scoped per token. https://revnu.com/docs/authentication"
      },
      "contentToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "rvc_…",
        "description": "Read-only Content API token. https://revnu.com/docs/content-api"
      },
      "eventKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "rsk_live_…",
        "description": "Server-side event key for the Event API. https://revnu.com/docs/event-api"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "A code such as `forbidden`, or a sentence for a 400."
          },
          "message": {
            "type": "string"
          },
          "requiredScope": {
            "type": "string",
            "description": "On 403: the scope the route needs."
          },
          "actions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "On 409 invalid_action: the verbs the card accepts now."
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request is malformed; the error names the problem.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing, malformed, unknown or revoked token.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "The token lacks the scope in `requiredScope`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Unknown id, or another account's id.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Conflict": {
        "description": "A settled refusal: `invalid_action` (see `actions`) or `rejected`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limited.",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds."
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unavailable": {
        "description": "Temporarily unavailable; retry the same call.",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds."
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}
