# Victoria AI Docs: full documentation > Documentation for Victoria AI, an outbound AI SDR that runs LinkedIn and email sequences from your own accounts. The Help Center covers using the app, the Playbook covers outbound practice with example sequences, and the API reference and guides cover the REST API (leads, campaigns and sequences, analytics, CRM pipelines and deals, reply webhooks) and the MCP server. - Base URL: `https://api.versionseven.ai/v1`. Requests and responses are JSON. - Authentication: send `Authorization: Bearer ` on every request. Keys start with `vk_`, are generated in the Victoria AI app under Settings → API Keys, and each belongs to one organization. - Errors share one body, `{"success": false, "error": "CODE", "message": "…", "details": {…}, "request_id": "…"}`. Branch on `error`; `details` is only present for some errors. - List endpoints page with `limit` and `offset` and return `count`, `total`, `limit`, `offset` and `has_more`. - Rate limits: 100 requests a minute to each endpoint and 600 a minute in total, per API key. Going over answers `429 RATE_LIMITED` with a `Retry-After` header. - Create requests (`POST /v1/leads`, `POST /v1/campaigns`, `POST /v1/crm/deals`) accept an `Idempotency-Key` header, which makes retries safe. - Webhooks: the `prospect_response` event is sent to a campaign's webhooks when a prospect replies. Deliveries from a webhook with a secret carry `X-Signature-256: sha256=`. Deduplicate on the payload's `idempotency_key`. - The OpenAPI specification is served at `https://api.versionseven.ai/openapi.json`. - MCP server: `https://api.versionseven.ai/mcp`. To connect Claude (claude.ai, the desktop app or Cowork): Settings → Connectors → Add custom connector, paste the URL, then sign in to Victoria AI, choose Read and write (builds and launches campaigns, confirming each change) or Read only, and choose Allow. To connect ChatGPT: Settings → Connectors → Create (developer mode on), same URL, same sign-in. No API key is involved. Claude Code: `claude mcp add --transport http victoria https://api.versionseven.ai/mcp` then `/mcp` to sign in. Cursor and scripts send an API key as the bearer header. Guide: `https://docs.versionseven.ai/guides/connect-your-ai`. - Using the Victoria AI app (senders, leads, campaigns, the Appointment Setter, billing, troubleshooting) is covered by the Help Center at `https://docs.versionseven.ai/help`; outbound practice and example sequences by the Playbook at `https://docs.versionseven.ai/playbook`. Both are listed below. - Product, pricing and feature descriptions live on the marketing site: `https://www.versionseven.ai/llms.txt` is the index and `https://www.versionseven.ai/llms-full.txt` the full text. Any page there has a Markdown export at its URL with `.md` added. - Victoria Pulse has a separate API and MCP server: its OpenAPI document is at `https://data.versionseven.ai/openapi.json` and its MCP endpoint at `https://data.versionseven.ai/mcp`. --- Source: https://docs.versionseven.ai/guides/quickstart # Quickstart Create an API key, verify it, and add your first lead to a campaign with the Victoria AI REST API. This guide takes you from a new API key to a lead enrolled in a campaign. You need a Victoria AI account with at least one campaign. ## Base URL Every endpoint lives under one versioned base URL: ```text https://api.versionseven.ai/v1 ``` ## 1. Create an API key Create a key in the Victoria AI app under **Settings → API Keys**. Organization owners and admins can manage keys. Keys start with `vk_`, and the full key is shown only once, when you create it, so store it somewhere safe, such as a secret manager. The examples below read the key from an environment variable: ```bash export VICTORIA_API_KEY="vk_your_api_key" ``` ## 2. Verify the key [`GET /v1/auth/verify`](https://docs.versionseven.ai/api-reference/auth/verify-api-key) confirms the key works and returns the organization it belongs to. It needs no scope, which makes it the right first call from a new integration. ```bash curl https://api.versionseven.ai/v1/auth/verify \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` ```json { "success": true, "message": "API key is valid", "organization_id": "7f1c2b9e-4d3a-4f6b-9a2e-1c5d8e7f9a0b", "organization_name": "Acme" } ``` A missing or unknown key answers `401 UNAUTHORIZED`. [Authentication](https://docs.versionseven.ai/guides/authentication) covers keys, scopes and every authentication error. ## 3. Find a campaign [`GET /v1/campaigns`](https://docs.versionseven.ai/api-reference/campaigns/list-campaigns) lists your campaigns, newest first. Note the `id` of the campaign you want to add a lead to. ```bash curl "https://api.versionseven.ai/v1/campaigns?limit=20" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` ## 4. Add a lead to the campaign [`POST /v1/leads`](https://docs.versionseven.ai/api-reference/leads/create-lead) creates a lead and, because the body includes `campaign_id`, enrols it in that campaign. A lead needs `first_name`, `last_name`, and an `email` or `linkedin_url`. ```bash curl -X POST https://api.versionseven.ai/v1/leads \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "lead": { "first_name": "Sarah", "last_name": "Johnson", "email": "sarah.johnson@acmecorp.com", "company": "Acme Corp", "title": "VP of Sales" } }' ``` The `Idempotency-Key` header makes the request safe to retry. If the connection drops and you send the request again with the same key, you get the original response back instead of a second attempt. See [Idempotency](https://docs.versionseven.ai/guides/idempotency). A `201` response carries the new lead and its enrolment. The `lead` object below is shortened: ```json { "success": true, "message": "Lead successfully added to campaign", "lead_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "sequence_lead_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "enrollment": { "sequence_lead_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "campaign_id": "550e8400-e29b-41d4-a716-446655440000" }, "lead": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "first_name": "Sarah", "last_name": "Johnson", "email": "sarah.johnson@acmecorp.com", "company": "Acme Corp", "title": "VP of Sales" } } ``` The enrolment starts in the campaign's backlog, and the lead is contacted once the campaign has sending capacity for it. If a lead with the same email or LinkedIn URL already exists in your organization, that lead is enrolled instead, and the response is `200` with `lead_created: false`. Sending a lead that's already in the campaign answers `409 LEAD_ALREADY_IN_CAMPAIGN`. ## 5. Hear when the lead replies Register a webhook on the campaign with [`POST /v1/campaigns/{campaign_id}/webhooks`](https://docs.versionseven.ai/api-reference/campaigns/create-webhook), and Victoria AI sends a [`prospect_response`](https://docs.versionseven.ai/api-reference/webhooks/prospect-response) event to your URL when a prospect replies. [Receiving webhooks](https://docs.versionseven.ai/guides/webhooks) walks through the setup, and [Verifying signatures](https://docs.versionseven.ai/guides/verifying-webhooks) shows how to confirm each delivery came from Victoria AI. ## Next steps - [Connect your AI](https://docs.versionseven.ai/guides/connect-your-ai): do all of this from Claude, ChatGPT or Claude Code instead, signing in once and approving each write. - [Set up a campaign by API](https://docs.versionseven.ai/guides/set-up-a-campaign): create a campaign, write its sequence, add AI fields and senders, and activate it. - [Errors](https://docs.versionseven.ai/guides/errors): the error body and what every error code means. - [Pagination](https://docs.versionseven.ai/guides/pagination) and [Rate limits](https://docs.versionseven.ai/guides/rate-limits), before you sync large lists. - The [API Reference](https://docs.versionseven.ai/api-reference), for every endpoint. --- Source: https://docs.versionseven.ai/guides/authentication # Authentication Create and use API keys, understand the organization and scopes a key covers, and handle authentication errors. Every request to the Victoria AI API is authenticated with an API key, sent as a Bearer token. A key belongs to one organization, and every request made with it reads and writes that organization's data. ## Create a key In the Victoria AI app, open **Settings → API Keys** and generate a key. Organization owners and admins can manage keys. - Keys start with `vk_`. - The full key is shown once, when you generate it. Victoria AI stores only a hash of the key, so it can't be shown again. Copy it into a secret manager or your deployment's environment variables straight away. - Give each integration its own key, so you can revoke one without breaking the others. ## Send the key Put the key in the `Authorization` header of every request, after the word `Bearer` and a single space: ```bash curl https://api.versionseven.ai/v1/auth/verify \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` [`GET /v1/auth/verify`](https://docs.versionseven.ai/api-reference/auth/verify-api-key) needs no scope and returns the organization the key belongs to, which makes it a quick way to check a key. Keep keys on your server. Don't put them in browser code or mobile apps, where anyone can read them. ## Organizations A key only ever sees its own organization's data. An ID that belongs to another organization is treated as if it doesn't exist: - In the path, it answers a not-found error, such as `404 LEAD_NOT_FOUND`. - In a request body, such as the `stage_id` of a deal, it answers `400`, such as `400 INVALID_STAGE`. Neither response confirms that the ID exists somewhere else. ## Scopes Each endpoint requires a scope, made of a resource family and an access level. | Family | Covers | | - | - | | `leads` | Leads and their campaign enrolments | | `campaigns` | Campaigns, sequences, analytics, queues, webhooks and reference data | | `crm` | Pipelines and deals | | `accounts` | Connected sender accounts | The level is `read` or `write`, and `write` includes `read`: a key with `leads:write` can also call endpoints that need `leads:read`. A key without the scope an endpoint needs answers `403 INSUFFICIENT_SCOPE`. > **Note:** Keys generated in the Victoria AI app today have every scope, so they can call every endpoint. | Endpoint | Required scope | | - | - | | [`GET /v1/auth/verify`](https://docs.versionseven.ai/api-reference/auth/verify-api-key) | None | | [`GET /v1/accounts`](https://docs.versionseven.ai/api-reference/accounts/list-accounts) | `accounts:read` | | [`POST /v1/accounts/connect-link`](https://docs.versionseven.ai/api-reference/accounts/create-connect-link) | `accounts:write` | | [`POST /v1/leads`](https://docs.versionseven.ai/api-reference/leads/create-lead) | `leads:write` | | [`GET /v1/leads`](https://docs.versionseven.ai/api-reference/leads/list-leads) | `leads:read` | | [`GET /v1/leads/{lead_id}`](https://docs.versionseven.ai/api-reference/leads/retrieve-lead) | `leads:read` | | [`PATCH /v1/leads/{lead_id}`](https://docs.versionseven.ai/api-reference/leads/update-lead) | `leads:write` | | [`DELETE /v1/leads/{lead_id}`](https://docs.versionseven.ai/api-reference/leads/remove-lead-from-campaign) | `leads:write` | | [`GET /v1/campaigns`](https://docs.versionseven.ai/api-reference/campaigns/list-campaigns) | `campaigns:read` | | [`POST /v1/campaigns`](https://docs.versionseven.ai/api-reference/campaigns/create-campaign) | `campaigns:write` | | [`GET /v1/campaigns/{campaign_id}`](https://docs.versionseven.ai/api-reference/campaigns/retrieve-campaign) | `campaigns:read` | | [`PATCH /v1/campaigns/{campaign_id}`](https://docs.versionseven.ai/api-reference/campaigns/update-campaign) | `campaigns:write` | | [`PUT /v1/campaigns/{campaign_id}/sequence`](https://docs.versionseven.ai/api-reference/campaigns/replace-sequence) | `campaigns:write` | | [`PATCH /v1/campaigns/{campaign_id}/sequence/steps/{step_id}`](https://docs.versionseven.ai/api-reference/campaigns/update-sequence-step) | `campaigns:write` | | [`GET /v1/campaigns/{campaign_id}/analysis`](https://docs.versionseven.ai/api-reference/campaigns/get-campaign-analysis) | `campaigns:read` | | [`GET /v1/campaigns/{campaign_id}/queue`](https://docs.versionseven.ai/api-reference/campaigns/get-campaign-queue) | `campaigns:read` | | [`GET /v1/campaigns/{campaign_id}/preflight`](https://docs.versionseven.ai/api-reference/campaigns/get-campaign-preflight) | `campaigns:read` | | [`PUT /v1/campaigns/{campaign_id}/senders`](https://docs.versionseven.ai/api-reference/campaigns/assign-senders) | `campaigns:write` | | [`GET /v1/campaigns/{campaign_id}/ai-fields`](https://docs.versionseven.ai/api-reference/campaigns/list-ai-fields) | `campaigns:read` | | [`POST /v1/campaigns/{campaign_id}/ai-fields`](https://docs.versionseven.ai/api-reference/campaigns/create-ai-field) | `campaigns:write` | | [`PATCH /v1/campaigns/{campaign_id}/ai-fields/{field_id}`](https://docs.versionseven.ai/api-reference/campaigns/update-ai-field) | `campaigns:write` | | [`GET /v1/campaigns/{campaign_id}/responder`](https://docs.versionseven.ai/api-reference/campaigns/get-responder) | `campaigns:read` | | [`PATCH /v1/campaigns/{campaign_id}/responder`](https://docs.versionseven.ai/api-reference/campaigns/update-responder) | `campaigns:write` | | [`POST /v1/campaigns/{campaign_id}/preview`](https://docs.versionseven.ai/api-reference/campaigns/preview-campaign) | `campaigns:read` | | [`GET /v1/campaigns/{campaign_id}/bookings`](https://docs.versionseven.ai/api-reference/campaigns/list-bookings) | `campaigns:read` | | [`POST /v1/campaigns/{campaign_id}/ab-testing`](https://docs.versionseven.ai/api-reference/campaigns/set-ab-testing) | `campaigns:write` | | [`POST /v1/campaigns/{campaign_id}/ab-testing/promote`](https://docs.versionseven.ai/api-reference/campaigns/promote-ab-winner) | `campaigns:write` | | [`GET /v1/campaigns/{campaign_id}/daily-stats`](https://docs.versionseven.ai/api-reference/campaigns/get-daily-stats) | `campaigns:read` | | [`GET /v1/campaigns/{campaign_id}/step-funnel`](https://docs.versionseven.ai/api-reference/campaigns/get-step-funnel) | `campaigns:read` | | [`GET /v1/campaigns/{campaign_id}/senders/breakdown`](https://docs.versionseven.ai/api-reference/campaigns/get-sender-breakdown) | `campaigns:read` | | [`GET /v1/campaigns/{campaign_id}/personalization-quality`](https://docs.versionseven.ai/api-reference/campaigns/get-personalization-quality) | `campaigns:read` | | [`GET /v1/campaigns/{campaign_id}/ab-cohorts`](https://docs.versionseven.ai/api-reference/campaigns/get-ab-cohorts) | `campaigns:read` | | [`GET /v1/campaigns/{campaign_id}/webhooks`](https://docs.versionseven.ai/api-reference/campaigns/list-webhooks) | `campaigns:read` | | [`POST /v1/campaigns/{campaign_id}/webhooks`](https://docs.versionseven.ai/api-reference/campaigns/create-webhook) | `campaigns:write` | | [`DELETE /v1/campaigns/{campaign_id}/webhooks/{webhook_id}`](https://docs.versionseven.ai/api-reference/campaigns/delete-webhook) | `campaigns:write` | | [`POST /v1/campaigns/{campaign_id}/webhooks/{webhook_id}/secret`](https://docs.versionseven.ai/api-reference/campaigns/rotate-webhook-secret) | `campaigns:write` | | [`GET /v1/crm/pipelines`](https://docs.versionseven.ai/api-reference/crm-pipelines/list-pipelines) | `crm:read` | | [`GET /v1/crm/pipelines/{pipeline_id}`](https://docs.versionseven.ai/api-reference/crm-pipelines/retrieve-pipeline) | `crm:read` | | [`PATCH /v1/crm/pipelines/{pipeline_id}`](https://docs.versionseven.ai/api-reference/crm-pipelines/update-pipeline) | `crm:write` | | [`GET /v1/crm/deals`](https://docs.versionseven.ai/api-reference/crm-deals/list-deals) | `crm:read` | | [`POST /v1/crm/deals`](https://docs.versionseven.ai/api-reference/crm-deals/create-deal) | `crm:write` | | [`GET /v1/crm/deals/{deal_id}`](https://docs.versionseven.ai/api-reference/crm-deals/retrieve-deal) | `crm:read` | | [`PATCH /v1/crm/deals/{deal_id}`](https://docs.versionseven.ai/api-reference/crm-deals/update-deal) | `crm:write` | | [`GET /v1/sequence-templates`](https://docs.versionseven.ai/api-reference/reference/list-sequence-templates) | `campaigns:read` | | [`GET /v1/webhooks/examples`](https://docs.versionseven.ai/api-reference/reference/list-webhook-examples) | `campaigns:read` | ## Rotate a key 1. Generate a new key in **Settings → API Keys**. 2. Deploy your integration with the new key. 3. Revoke the old key. Requests made with a revoked key answer `401 UNAUTHORIZED`. If a key is ever exposed, revoke it straight away. ## Authentication errors | Status | Code | Meaning | | - | - | - | | `401` | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the key is unknown or revoked. | | `401` | `API_KEY_EXPIRED` | The key is past its expiry date. | | `403` | `INSUFFICIENT_SCOPE` | The key doesn't have the scope the endpoint requires. | | `403` | `ORGANIZATION_DEACTIVATED` | The organization that owns the key has been deactivated. | | `503` | `AUTH_UNAVAILABLE` | The key couldn't be checked. Retry after a short wait. | A header counts as malformed unless it's exactly `Bearer`, one space, and the key. --- Source: https://docs.versionseven.ai/guides/connect-your-ai # Connect your AI Connect Claude, ChatGPT, Claude Code or any MCP host to your Victoria AI workspace, and what a connected assistant can and can't do. Victoria AI runs an [MCP](https://modelcontextprotocol.io) server at `https://api.versionseven.ai/mcp`. Connect it to an AI assistant and the assistant can create and edit campaigns, add leads, check senders, run the activation preflight and activate, with the same tools the Copilot uses inside the app. The conversation runs on the host's model and costs nothing on Victoria's side; only the two lead-finding tools spend credits. ## Connect in a minute Most people connect from Claude or ChatGPT in the browser, and it takes three steps. No key, nothing to install. 1. **Add the connector.** In Claude: **Settings → Connectors → Add custom connector**, paste `https://api.versionseven.ai/mcp`, save. The Claude desktop app and Cowork use the same Connectors setting, so one connection covers all three. In ChatGPT: **Settings → Connectors → Create** (developer mode on), paste the same URL. 2. **Sign in and approve.** The assistant opens Victoria AI. Sign in with your Victoria account, pick your workspace if you have more than one, choose **Read and write** (it can build and launch campaigns, asking before each change) or **Read only** (it can look but never change anything or spend credits), and choose **Allow**. 3. **Ask for something.** Start a chat with the connector on and try _"Which of my campaigns got replies this week?"_ — it reads and changes nothing. From there, ask for a campaign. That's the whole setup. The rest of this page is what the connection can do, how it asks before changing anything, and the other ways to connect. ## What a connected assistant can do The server lists a tool when your credential holds its scope. A **Read and write** connection holds every scope, so all of these are listed; a **Read only** connection lists only the reads. To change a connection's access, disconnect it in **Settings → Connect your AI** and connect again. - **Reads**, which have no side effects: `campaigns_list`, `campaign_detail`, `campaign_analysis`, `campaign_step_funnel`, `campaign_sender_breakdown`, `campaign_queue`, `campaign_preflight`, `personalization_quality`, `get_sequence_template`, `list_personalization_fields`, `list_webhooks`, `onboarding_checklist`, `read_website`, `leads_lookup`, `lead_detail`, `csv_upload_status`, `lead_jobs_status`, `sender_accounts`, `verify_sender`, `connect_sender`, `reconnect_sender`, `check_domain_dns` (an SPF, DKIM and DMARC check of any domain; nothing is stored), `pipelines_lookup`, `deals_lookup`, `deal_detail` and `team_members_lookup`. `connect_sender` and `reconnect_sender` answer with a link into the app and mint nothing themselves. ChatGPT's deep research gets `search` and `fetch`, which find campaigns, leads and deals by name, email or company and read one record. - **Writes**, which the host asks you to confirm: `create_campaign`, `update_campaign`, `update_sequence`, `update_sequence_step`, `create_personalization_field`, `update_personalization_field`, `assign_sender_accounts`, `set_ab_testing`, `promote_ab_winner`, `activate_campaign`, `activate_webhook`, `deactivate_webhook`, `update_lead`, `create_deal`, `update_deal`, and `check_sender_dns`, which re-runs the SPF, DKIM and DMARC check for a connected mailbox and stores the verdict (it needs `accounts:write`). - **Actions that spend credits**, also confirmed: `find_leads` and `add_leads_from_search`, which search the lead database and add matches to a campaign. The inbox and the Copilot's knowledge-base tools aren't exposed. One resource, `victoria://guides/messaging-rules`, holds the sequence format and the house messaging rules; an assistant reads it before writing sequence copy. ### Scopes An API key can be limited per family (`leads`, `campaigns`, `crm`, `accounts`) to `read` or `write`, and `:write` implies `:read`, so a key with `campaigns:read` alone lists the campaign reads and none of the campaign writes. An OAuth connection carries every scope. [Authentication](https://docs.versionseven.ai/guides/authentication#scopes) covers the scopes themselves. ### More than one organization A connection or key acts in one organization. If you belong to more than one, choose a default on the consent screen when you connect, or later in **Settings → Connect your AI**, or ask the assistant: the `switch_organization` tool lists your organizations and moves the connection to one of them, and later calls act there. Until a default is set, every other tool refuses with `NO_DEFAULT_ORGANIZATION`. ## How the sign-in works The three steps above are the whole procedure; this is what happens underneath. The consent page names the host asking, where you'll be sent back to, and what the connection can do; if you've already approved that host, the page sends you straight back. The connection acts as you, in your organization, with every scope. Hosts find the sign-in flow themselves: the discovery document at `https://api.versionseven.ai/.well-known/oauth-protected-resource/mcp` names Victoria AI's authorization server, and an unauthenticated request to `/mcp` answers `401` with a `WWW-Authenticate` header pointing at it. A token copied from a browser session is refused: only a token issued through this consent flow, or an API key, is accepted. ## Connect Claude Code Claude Code signs in the same way. Add the server, then run `/mcp` inside Claude Code and choose it to complete the sign-in: ```bash claude mcp add --transport http victoria https://api.versionseven.ai/mcp ``` Add `--scope user` to make it available in every project rather than the current one. ## Cursor and scripts Hosts that take a URL and a header send an API key as the bearer token instead (Claude Code accepts this too, for a machine that shouldn't hold a sign-in). [Create a key](https://docs.versionseven.ai/guides/authentication#create-a-key) in **Settings → API Keys**, then: **Claude Code** ```bash claude mcp add --transport http victoria https://api.versionseven.ai/mcp \ --header "Authorization: Bearer $VICTORIA_API_KEY" ``` **Cursor** ```json { "mcpServers": { "victoria": { "url": "https://api.versionseven.ai/mcp", "headers": { "Authorization": "Bearer vk_your_key" } } } } ``` **curl** ```bash curl https://api.versionseven.ai/mcp \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}' ``` For Cursor, save the JSON as `.cursor/mcp.json` in your project (or `~/.cursor/mcp.json` for every project) with your key in place of `vk_your_key`, and keep the file out of version control. The transport is Streamable HTTP at its plainest: each request is a `POST` carrying one JSON-RPC message, answered with a JSON body rather than an event stream. The server is stateless, so there's no session to open; `tools/list` and `tools/call` are independent requests, as above. The key's [scopes](https://docs.versionseven.ai/guides/authentication#scopes) decide which tools are listed. `find_leads`, `add_leads_from_search` and `create_deal` act as a person: with a key, that's the user who generated it or, when the key records no creator, the organization's owner. ## What it never does on its own - **Every write is confirmed.** Each tool that changes your account is annotated as a write, which is what hosts such as claude.ai and ChatGPT use to ask you before running it. - **Activation runs the preflight.** `activate_campaign` goes through the same readiness checks as the Activate button in the app and [`PATCH /v1/campaigns/{campaign_id}`](https://docs.versionseven.ai/api-reference/campaigns/update-campaign): sequence and step content, variables and lead data, assigned sender accounts and channel fit, sender email authentication, account conflicts and limits, enrolled leads, and the subscription. A blocking check refuses; warnings need an explicit `ack_warnings`. Campaigns are created as drafts, and nothing sends until activation. - **It never sends a message.** No tool sends to or replies to a prospect, and the inbox isn't exposed. - **It can't connect a sender, upload a CSV or change billing.** Those happen in the app; a result that needs one carries a `next_step` link to the right page. ## Credits Two tools spend credits, and they quote the cost before you approve: - `find_leads`: about 0.8 credits per result returned, so a page of 25 is about 21 credits. Short pages are refunded. - `add_leads_from_search`: the search cost plus, in `email` mode, about 3.3 credits per lead reserved for finding an email and settled to what's found. `linkedin_only` mode costs only the search. A credit is 1,000 tokens. Both tools are refused at a zero balance and past the trial's lead cap; `add_leads_from_search` then answers a `suggested_count` that fits. Leads added this way are personalized and worked like any other lead, which spends credits as usual. ## Where lead data comes from Results from `find_leads` and `add_leads_from_search` are licensed from FullEnrich and are shown with the attribution "Powered by FullEnrich". Use them only for your own B2B outreach inside your workspace: they can't be exported for resale or shared outside your organization. If the GDPR applies to your outreach, tell each contact where their data came from within a month of obtaining it or at your first message, whichever is earlier. Leads you delete, and every lead in an account that closes, are purged within 90 days, including from backups. The [Terms of Service](https://www.versionseven.ai/legal/terms) and [Acceptable Use Policy](https://www.versionseven.ai/legal/aup) carry the full conditions. ## Limits and errors Lead-database spend through connected assistants and API keys (`find_leads`, `add_leads_from_search`) is also capped per organization at 5,000 credits in any 24 hours; past it the tool answers `DAILY_SPEND_LIMIT` with what is left. The Lead Database page and the in-app Copilot are not capped. Each signed-in user, and each API key, gets 120 tool calls a minute and 2,000 a day; connecting Claude and ChatGPT as the same user shares one allowance. Over either, the tool answers an error result with `error: "RATE_LIMITED"` and `retry_after_seconds` rather than an HTTP `429`, so the assistant can wait and retry. The [per-address limit](https://docs.versionseven.ai/guides/rate-limits) on the REST API also applies to `/mcp`, before authentication, as a real `429` with `Retry-After`. | Status | Meaning | | - | - | | `401` | No credential, or one that isn't valid: a browser session token, a revoked or expired key. The `WWW-Authenticate` header names the discovery document, which is how a host starts the sign-in flow. | | `403` | `ORGANIZATION_DEACTIVATED`, in the API's [error body](https://docs.versionseven.ai/guides/errors#the-error-body). | | `429` | `RATE_LIMITED` for the address. Retry after `Retry-After` seconds. | | `503` | `AUTH_UNAVAILABLE`: the credential couldn't be checked. Retry after a short wait. | Inside a conversation, a refused call is a tool result with `success: false` and an `error` code: `INSUFFICIENT_SCOPE`, `RATE_LIMITED`, `DAILY_SPEND_LIMIT`, `NO_ACTING_USER`, `NO_DEFAULT_ORGANIZATION`, `INSUFFICIENT_TOKENS`, `UNKNOWN_TOOL`, `INTERNAL_ERROR`, or the tool's own code in the same shape, such as `CAMPAIGN_NOT_READY` from `activate_campaign`. ## Revoking access - An OAuth connection is removed from your assistant's connector settings. - An API key is revoked in **Settings → API Keys**. The key is checked on every call, so a revoked key answers `401` from the next request. ## Victoria Pulse Victoria Pulse has its own API and MCP server, separate from this one. Its OpenAPI document is at `https://data.versionseven.ai/openapi.json`, its tool manifest at `https://data.versionseven.ai/openapi/mcp-tools.json`, and its MCP endpoint at `https://data.versionseven.ai/mcp`, with the discovery document at `https://data.versionseven.ai/.well-known/oauth-protected-resource/mcp`. --- Source: https://docs.versionseven.ai/guides/set-up-a-campaign # Set up a campaign by API Create a campaign, write its sequence, add AI fields and senders, check the activation preflight, and activate it, all from the REST API. Everything the Victoria AI app does to take a campaign from draft to sending is available by API. This guide runs the steps in the order the activation preflight expects them. You need a key with `campaigns:write`, `leads:write` and, to connect a sender, `accounts:write`. ## 1. Create the campaign [`POST /v1/campaigns`](https://docs.versionseven.ai/api-reference/campaigns/create-campaign) creates a campaign in draft, with `is_active: false`. Nothing sends until you activate it. ```bash curl -X POST https://api.versionseven.ai/v1/campaigns \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"name": "Q3 outbound: heads of sales"}' ``` Keep the campaign's `id` from the response; every call below takes it in the path. ## 2. Write the sequence [`PUT /v1/campaigns/{campaign_id}/sequence`](https://docs.versionseven.ai/api-reference/campaigns/replace-sequence) replaces the campaign's whole sequence. Start from a blank template from [`GET /v1/sequence-templates`](https://docs.versionseven.ai/api-reference/reference/list-sequence-templates) and fill in each step's `message`, or `subject` and `content` for email. The sequence is validated before it's saved; a rule that fails answers `400 SEQUENCE_VALIDATION_FAILED` with every failing rule in the message. Step copy can use lead fields such as `{first_name}` and `{company}`, and any AI field you add in the next step. ## 3. Add AI fields An AI field is a variable written for each lead before its first message, from the lead's LinkedIn profile or company website. [`POST /v1/campaigns/{campaign_id}/ai-fields`](https://docs.versionseven.ai/api-reference/campaigns/create-ai-field) creates one; use it in the sequence as `{field_name}`. ```bash curl -X POST https://api.versionseven.ai/v1/campaigns/$CAMPAIGN_ID/ai-fields \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "field_name": "recent_news", "ai_instructions": "In one short sentence, name something specific the company announced or shipped recently, from its website.", "fallback_value": "the work your team is doing", "data_sources": ["website"] }' ``` `fallback_value` is required: it's what a lead gets when personalization fails. The preflight fails a sequence that uses a variable no AI field or lead field provides. [`PATCH /v1/campaigns/{campaign_id}/ai-fields/{field_id}`](https://docs.versionseven.ai/api-reference/campaigns/update-ai-field) changes a field later, or sets `is_active: false` to stop generating it. ## 4. Assign senders [`PUT /v1/campaigns/{campaign_id}/senders`](https://docs.versionseven.ai/api-reference/campaigns/assign-senders) sets the accounts that send for the campaign, from the `id` values in [`GET /v1/accounts`](https://docs.versionseven.ai/api-reference/accounts/list-accounts). Each must be connected and not already sending for another active campaign. ```bash curl -X PUT https://api.versionseven.ai/v1/campaigns/$CAMPAIGN_ID/senders \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"account_ids": ["0f2f5b9e-8d3a-4c6b-9a2e-1c5d8e7f9a0b"]}' ``` If the account you need isn't connected yet, or shows `is_active: false`, [`POST /v1/accounts/connect-link`](https://docs.versionseven.ai/api-reference/accounts/create-connect-link) mints a hosted sign-in link. Send its `url` to the person whose LinkedIn or mailbox it is; the account appears in [`GET /v1/accounts`](https://docs.versionseven.ai/api-reference/accounts/list-accounts) once they've signed in. Pass `reconnect_account_id` to re-authenticate an existing account instead of adding one. ## 5. Add leads Enrol leads with [`POST /v1/leads`](https://docs.versionseven.ai/api-reference/leads/create-lead) and a `campaign_id`, as in the [Quickstart](https://docs.versionseven.ai/guides/quickstart#4-add-a-lead-to-the-campaign). The preflight blocks activation of a campaign with no enrolled leads. ## 6. Check the preflight [`GET /v1/campaigns/{campaign_id}/preflight`](https://docs.versionseven.ai/api-reference/campaigns/get-campaign-preflight) runs the readiness checks without activating. `ok: true` means nothing blocks; each entry in `checks` is `pass`, `warn` or `fail`, and `issues` says what to fix. ```bash curl https://api.versionseven.ai/v1/campaigns/$CAMPAIGN_ID/preflight \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` [`POST /v1/campaigns/{campaign_id}/preview`](https://docs.versionseven.ai/api-reference/campaigns/preview-campaign) renders variation A for a few enrolled leads, with every AI field at its fallback value, so you can read the worst-case message before it goes out. It spends no credits. ## 7. Activate [`PATCH /v1/campaigns/{campaign_id}`](https://docs.versionseven.ai/api-reference/campaigns/update-campaign) with `is_active: true` runs the same preflight and activates when it passes. A blocking check answers `409 CAMPAIGN_NOT_READY`; warnings alone answer `409 CAMPAIGN_ACTIVATION_WARNINGS` until you repeat the request with `ack_warnings: true`. ```bash curl -X PATCH https://api.versionseven.ai/v1/campaigns/$CAMPAIGN_ID \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"is_active": true}' ``` Pause with `is_active: false`; pausing never runs the preflight. ## Optional: the reply agent and A/B tests - [`PATCH /v1/campaigns/{campaign_id}/responder`](https://docs.versionseven.ai/api-reference/campaigns/update-responder) turns the AI Appointment Setter on and sets where it sends interested prospects (`goal_link`), the links it may share and its instructions. [`GET /v1/campaigns/{campaign_id}/bookings`](https://docs.versionseven.ai/api-reference/campaigns/list-bookings) then lists the meetings it books. - To test two versions of the sequence, write a `variation_b` in the sequence, turn the test on with [`POST /v1/campaigns/{campaign_id}/ab-testing`](https://docs.versionseven.ai/api-reference/campaigns/set-ab-testing), compare the arms with [`GET /v1/campaigns/{campaign_id}/ab-cohorts`](https://docs.versionseven.ai/api-reference/campaigns/get-ab-cohorts), and end the test with [`POST /v1/campaigns/{campaign_id}/ab-testing/promote`](https://docs.versionseven.ai/api-reference/campaigns/promote-ab-winner). --- Source: https://docs.versionseven.ai/guides/ai-personalization-fields # AI Personalization fields What an AI Personalization field is, how Victoria AI fills it for each lead, what happens when the research finds nothing, and the endpoints that manage fields. An AI Personalization field is a per-campaign variable that Victoria AI writes for each lead before its message goes out. A field is used as `{field_name}` in any step of the sequence, including an email subject line, next to the lead fields such as `{first_name}` and `{company}`. ## How a field is filled Each field has instructions (what to write, and from what), 1 or 2 sources, and a fallback. The sources are the lead's company website and the lead's LinkedIn profile; a field can use either or both. At send time, the field is written from 3 inputs: the lead's own columns, 1 live read of the company website, and the LinkedIn profile with up to 5 recent posts. The model writes only what those inputs support. It never invents a fact about the lead or the company. ## When the research finds nothing When the sources don't support the field, the app works down a ladder: 1. The research result, when the sources support one. 2. The field's fallback, when one is set. 3. A plain line built from the lead's name, title, company, industry and size. 4. When none of those produces a usable value, the lead is held and retried after 24 hours. A held lead is not skipped; it is contacted once a later attempt fills the field. ## The API surface The REST API manages a campaign's fields. Generating them spends credits when the campaign sends, not when you call these endpoints (see [Credits](https://docs.versionseven.ai/guides/credits)). | Task | Endpoint | | - | - | | List a campaign's fields | [`GET /v1/campaigns/{campaign_id}/ai-fields`](https://docs.versionseven.ai/api-reference/campaigns/list-ai-fields) | | Create a field | [`POST /v1/campaigns/{campaign_id}/ai-fields`](https://docs.versionseven.ai/api-reference/campaigns/create-ai-field) | | Change a field, or stop generating it | [`PATCH /v1/campaigns/{campaign_id}/ai-fields/{field_id}`](https://docs.versionseven.ai/api-reference/campaigns/update-ai-field) | | Render the sequence with every field at its fallback | [`POST /v1/campaigns/{campaign_id}/preview`](https://docs.versionseven.ai/api-reference/campaigns/preview-campaign) | Creating a field takes `field_name` (letters, digits and underscores, up to 40 characters), `ai_instructions` (up to 4,000 characters), `fallback_value` (up to 300 characters), an optional `field_description` (up to 500 characters) and `data_sources`, which is `["linkedin"]`, `["website"]` or both, and defaults to both. An active field with the same name on the campaign answers `409 AI_FIELD_EXISTS`. Updating a field changes its instructions, fallback, description or sources. `field_name` can't be changed; create a new field instead. There is no delete: set `is_active: false` to stop generating the field for new leads. > **Note:** The API requires `fallback_value` on every new field, and caps it at 300 characters. The app treats the fallback as optional, allows up to 400 characters, and holds a lead whose field has no value. When you create fields through the API, pass a fallback so the request is accepted and the lead is never held for want of one. The preview endpoint renders variation A for up to 10 enrolled leads with every AI field at its fallback value, so you read the worst-case message before activation. It spends no credits, and it lists the fields it rendered at fallback in `ai_fields_at_fallback`. The preflight ([`GET /v1/campaigns/{campaign_id}/preflight`](https://docs.versionseven.ai/api-reference/campaigns/get-campaign-preflight)) fails a sequence that uses a variable no AI field or lead field provides. [Set up a campaign by API](https://docs.versionseven.ai/guides/set-up-a-campaign#3-add-ai-fields) walks through creating a field alongside the rest of the campaign. ## Personalization quality A connected assistant can read `personalization_quality` and `list_personalization_fields` over MCP (see [Connect your AI](https://docs.versionseven.ai/guides/connect-your-ai)). The REST API has no endpoint that reports how a field's values turned out; the app shows them on the campaign's leads. --- Source: https://docs.versionseven.ai/guides/ai-appointment-setter # AI Appointment Setter How the AI Appointment Setter handles a reply, when it hands off to a person, and how integrations configure it and hear about replies. The AI Appointment Setter is the reply agent on a campaign. When a prospect replies, it decides what to do next and works toward 1 goal: a meeting booked through your booking link. ## What it does with a reply On every reply, the agent decides 1 of 5 actions: reply, wait, nudge, escalate, or close. It records the reply's sentiment and whether the reply is an out-of-office message. A campaign runs it in 1 of 2 modes: autonomous, where it sends its own replies, or notify-only, where it reads each reply and leaves the sending to you. It books through your booking link and never proposes times itself. By default it sends up to 5 follow-ups to a prospect who goes quiet, on days 2, 3, 5, 7 and 10 after the last message; the schedule is configurable in the app, up to 10 follow-ups. ## When it hands off to a person The agent escalates the conversation when the prospect: - asks for a person; - raises pricing, legal, security or a complaint; - asks about something outside the material the agent was given; - names a colleague to talk to instead. It also stops after a reply limit, 12 replies per conversation by default. The reason is recorded with the conversation and delivered in the webhook as `escalation_reason`. A meeting booked through the link is detected by the app's calendar integration and listed on the campaign's bookings, described below. ## Configure it by API The agent is configured per campaign. [`GET /v1/campaigns/{campaign_id}/responder`](https://docs.versionseven.ai/api-reference/campaigns/get-responder) reads the configuration and [`PATCH /v1/campaigns/{campaign_id}/responder`](https://docs.versionseven.ai/api-reference/campaigns/update-responder) changes it: `is_enabled`, `goal_link` (an `https` URL the agent sends an interested prospect to), up to 10 `assets` (a `link` with a `description` of when to share it), `tone` (`friendly`, `direct` or `formal`), `custom_instructions`, `campaign_description`, `company_name` and `agent_name`. `is_enabled` also sets the campaign's `responder_enabled` flag, the one [`PATCH /v1/campaigns/{campaign_id}`](https://docs.versionseven.ai/api-reference/campaigns/update-campaign) toggles, so the two never disagree. The configuration the API returns also carries `mode`, `max_replies` and `agent_version`, but the update endpoint doesn't accept them: the mode, the reply limit and the follow-up schedule are set in the app. [`GET /v1/campaigns/{campaign_id}/bookings`](https://docs.versionseven.ai/api-reference/campaigns/list-bookings) lists the meetings booked from a campaign, newest first: calendar bookings with a `start_time` and `status`, and confirmations, where the agent or a person marked a lead as booked without a calendar event. Send `since` to count only bookings made at or after a time. [`GET /v1/campaigns/{campaign_id}/analysis`](https://docs.versionseven.ai/api-reference/campaigns/get-campaign-analysis) reports reply sentiment as aggregate counts. ## Hear about replies A prospect's reply is delivered to the campaign's webhooks as a [`prospect_response`](https://docs.versionseven.ai/api-reference/webhooks/prospect-response) event, once per lead per campaign and again when a later reply turns positive, with the prospect's message, the lead, and the agent's reading of the reply under `ai_response`: - `sentiment` and `out_of_office`; - `agent_action`: `reply`, `wait`, `nudge`, `escalate`, `close` or `skip`; - `conversation_status`: `awaiting_prospect`, `awaiting_followup`, `closed_won`, `closed_lost`, `closed_escalated` or `closed_no_response`; - `goal_status`: `not_sent`, `link_sent`, `soft_commit`, `confirmed` or `declined`; - `responder_message`, the reply the agent sent, or `null` when it sent nothing; - `escalation_reason`, when it handed off; - `sdr_brief`, a 1-line summary for a person. `agent_version` is `2` when the Appointment Setter handled the reply; feature-detect on it. When the agent didn't run, every field is `null` except `goal`, which carries the reason. [Receiving webhooks](https://docs.versionseven.ai/guides/webhooks) covers registration, retries and duplicates. ## The Unified Inbox is app-only There is no API or MCP endpoint that reads or sends inbox messages. The REST API exposes no conversation or message resource, and the MCP server lists no inbox tool. An integration that needs the conversation uses the webhook: each delivery carries the prospect's message and the agent's reply, and a later positive reply from the same lead arrives as a further event. --- Source: https://docs.versionseven.ai/guides/crm # Simple CRM The pipelines and deals in Victoria AI's Simple CRM, what a deal links to, what happens on a booked meeting, and the CRM endpoints. The Simple CRM tracks deals through pipelines. An organization has 1 or more pipelines, each with its own stages, and 1 of them is the default for new deals. ## Deals A deal has a name, a value in dollars, a win probability from 0 to 100, an expected close date, an actual close date once it closes, an owner (a member of your organization), notes and custom fields. Each deal links to 1 lead. A booked meeting moves the lead's existing deal forward. A positive reply does not create a deal on its own; you create deals yourself, in the app, by API, or through a connected assistant. There are no company records: a deal belongs to a lead, and the company is a field on the lead. ## Pipelines and stages by API | Task | Endpoint | Scope | | - | - | - | | List active pipelines | [`GET /v1/crm/pipelines`](https://docs.versionseven.ai/api-reference/crm-pipelines/list-pipelines) | `crm:read` | | Retrieve a pipeline with its stages | [`GET /v1/crm/pipelines/{pipeline_id}`](https://docs.versionseven.ai/api-reference/crm-pipelines/retrieve-pipeline) | `crm:read` | | Rename a pipeline, make it the default, or hide it | [`PATCH /v1/crm/pipelines/{pipeline_id}`](https://docs.versionseven.ai/api-reference/crm-pipelines/update-pipeline) | `crm:write` | Listing pipelines doesn't include stages; retrieve the pipeline to get them, in display order, each with an `id`, `name` and `display_order`. Setting `is_default: true` on a pipeline makes every other pipeline non-default. The API doesn't create pipelines or stages, and doesn't rename or reorder stages; that happens in the app. ## Deals by API | Task | Endpoint | Scope | | - | - | - | | List deals | [`GET /v1/crm/deals`](https://docs.versionseven.ai/api-reference/crm-deals/list-deals) | `crm:read` | | Create a deal | [`POST /v1/crm/deals`](https://docs.versionseven.ai/api-reference/crm-deals/create-deal) | `crm:write` | | Retrieve a deal | [`GET /v1/crm/deals/{deal_id}`](https://docs.versionseven.ai/api-reference/crm-deals/retrieve-deal) | `crm:read` | | Update a deal or move it to a stage | [`PATCH /v1/crm/deals/{deal_id}`](https://docs.versionseven.ai/api-reference/crm-deals/update-deal) | `crm:write` | Listing deals returns them newest first, each with a summary of its lead, filtered by `stage_id`, `owner_id` or `lead_id`, and paged with `limit` and `offset`. Creating a deal takes a `name` and either `lead_id` for an existing lead or an inline `lead` to create one with the deal, never both. `stage_id` defaults to the first stage in the pipeline. `value`, `probability`, `expected_close_date`, `owner_id`, `notes` and `metadata` are optional. The request accepts an `Idempotency-Key` header, which makes a retry safe (see [Idempotency](https://docs.versionseven.ai/guides/idempotency)). A `lead_id`, `stage_id` or `owner_id` that isn't in your organization answers `400 INVALID_LEAD`, `INVALID_STAGE` or `INVALID_OWNER`. Updating a deal changes only the fields you send. `stage_id` moves it to another stage, and `actual_close_date` records when it closed. The API doesn't delete deals. Going the other way, [Add CRM contacts to a campaign](https://docs.versionseven.ai/cookbook/add-crm-contacts) is a complete script that takes contacts exported from another CRM and enrols them in a Victoria AI campaign. ## From a connected assistant An assistant connected over MCP reads pipelines and deals with `pipelines_lookup`, `deals_lookup` and `deal_detail`, and creates or updates deals with `create_deal` and `update_deal`. The writes are annotated as such, so the host asks for your approval before each one runs. `create_deal` acts as a person: the user who connected, or, with an API key, the user who generated it. [Connect your AI](https://docs.versionseven.ai/guides/connect-your-ai) covers the setup. --- Source: https://docs.versionseven.ai/guides/lead-database # Lead database What the in-app Lead database searches, how emails are verified, what a search costs, and which parts are available over MCP and REST. The Lead database is the in-app search that finds people to add to a campaign. It is separate from your own leads, which the REST API creates and reads. ## Searching A search filters on job titles, seniority, locations, industries, company names, company domains and company size. A page holds at most 25 results. Results are added to a campaign in 1 of 2 modes: email and LinkedIn, where an email is looked up for each person, or LinkedIn only, where no email lookup runs. A bulk add takes up to 500 leads at a time. An email is kept only when the lookup rates it deliverable, high-probability or catch-all. Anything less is discarded, so a lead added in email mode either has an email that passed that check or has none. ## What it costs A search costs about 0.8 credits per result returned, so a page of 25 is about 21 credits; a short page is refunded to what was returned. Finding an email costs about 3.3 credits per lead, reserved when the add starts and refunded when no email is found. LinkedIn-only adds cost only the search. [Credits](https://docs.versionseven.ai/guides/credits) explains what a credit is and how balances work. ## From a connected assistant Two tools on the MCP server expose the database to a connected assistant, and they are the only programmatic way in: - `find_leads` runs a search and returns a page of results. - `add_leads_from_search` adds matches to a campaign, in `email` or `linkedin_only` mode. Both quote their cost before the host asks you to approve, and both are refused at a zero balance and past the trial's lead cap; `add_leads_from_search` then answers a `suggested_count` that fits. [Connect your AI](https://docs.versionseven.ai/guides/connect-your-ai#credits) has the details, and [Where lead data comes from](https://docs.versionseven.ai/guides/connect-your-ai#where-lead-data-comes-from) covers the licence terms for what the search returns. ## Bringing your own leads by REST The REST API has no search endpoint. It works with leads you bring: | Task | Endpoint | | - | - | | Create a lead, or enrol an existing one in a campaign | [`POST /v1/leads`](https://docs.versionseven.ai/api-reference/leads/create-lead) | | List your leads, filtered by campaign, industry, title, size or a search term | [`GET /v1/leads`](https://docs.versionseven.ai/api-reference/leads/list-leads) | | Retrieve a lead | [`GET /v1/leads/{lead_id}`](https://docs.versionseven.ai/api-reference/leads/retrieve-lead) | | Update a lead | [`PATCH /v1/leads/{lead_id}`](https://docs.versionseven.ai/api-reference/leads/update-lead) | | Delete a lead, or remove it from one campaign | [`DELETE /v1/leads/{lead_id}`](https://docs.versionseven.ai/api-reference/leads/remove-lead-from-campaign) | A lead needs a first name, a last name, and an email or LinkedIn URL. Leads added from the database appear in these endpoints like any other lead once they're in a campaign. Two campaign fields touch the database from REST: [`PATCH /v1/campaigns/{campaign_id}`](https://docs.versionseven.ai/api-reference/campaigns/update-campaign) replaces a campaign's saved `lead_database_filters` and its `refill_policy` (send `{"enabled": false}` to turn auto-refill off), and [`GET /v1/campaigns/{campaign_id}/queue`](https://docs.versionseven.ai/api-reference/campaigns/get-campaign-queue) reports the most recent lead-database refill alongside the backlog and days of runway. Neither runs a search. --- Source: https://docs.versionseven.ai/guides/credits # Credits What spends credits in Victoria AI, how balances roll over, and how an API or MCP integration finds out that the balance has run out. Credits pay for the AI work Victoria AI does on your behalf. 1 credit is 1,000 tokens. Every plan includes a monthly grant, and packs of credits can be bought on top. ## What spends credits | Work | Cost | | - | - | | Writing a lead's AI Personalization fields | About 3 credits per lead | | An AI Appointment Setter conversation | About 1.5 credits | | A Lead database search | About 0.8 credits per result | | An email found for a lead | About 3.3 credits | The figures are averages and depend on how much the model reads and writes. A search page that comes back short is refunded to what was returned, and an email that isn't found is refunded. No REST request spends credits at the moment you make it. Creating an AI field or turning the Appointment Setter on costs nothing until the campaign sends; the credits are spent as each lead is personalized and each reply is answered. Over MCP, `find_leads` and `add_leads_from_search` spend credits when they run, and quote the cost first. ## Balances and roll-over Purchased credits never expire. A plan's monthly grant rolls over 1 cycle: what you don't use this month is still there next month, up to 2 times the monthly amount in total. ## When the balance runs out Over MCP, a tool that would spend credits with no balance to spend answers a tool result with `success: false` and `error: "INSUFFICIENT_TOKENS"`. Treat it as a stop, not a retry: the call succeeds again only after credits are added in the app. The REST API has no error code for an empty balance, because no REST request spends credits at the time it's made. The related refusals are `403 NO_ACTIVE_SUBSCRIPTION`, when the organization has no active subscription, and `403 TRIAL_LEAD_CAP_REACHED`, when a trial has used its lead quota; `details` on the second carries the `cap`, `used` and `remaining` counts. Both are listed under [Error codes](https://docs.versionseven.ai/guides/errors#error-codes). ## Plan prices Plan prices, included credits and pack sizes are on the marketing site's pricing page, which has a Markdown export at [versionseven.ai/pricing.md](https://www.versionseven.ai/pricing.md). This guide doesn't repeat them. --- Source: https://docs.versionseven.ai/guides/errors # Errors The error response body, what each HTTP status means, how to read validation errors, and every error code the API returns. The API uses standard HTTP status codes, and every error response has the same JSON body. ## The error body ```json { "success": false, "error": "VALIDATION_ERROR", "message": "Request validation failed. Check the errors for details.", "details": { "errors": [ { "field": "lead.first_name", "message": "Field required", "type": "missing" } ] }, "request_id": "3b0f6c1e-8a2d-4e7b-9c5a-2d1f0e9b8a7c" } ``` | Field | Meaning | | - | - | | `success` | Always `false` on an error. | | `error` | A stable code for the kind of error. Branch on this. | | `message` | A human-readable explanation. Its wording can change, so don't parse it. | | `details` | Extra context for some errors, such as the failing fields of a validation error. Not always present. | | `request_id` | The request's ID, also sent as the `X-Request-ID` header. Quote it when you contact support. | ## Status codes | Status | Meaning | Retry? | | - | - | - | | `400` | The request is invalid. | No. Fix the request first. | | `401` | The API key is missing, invalid, revoked or expired. | No. | | `403` | The key lacks the required scope, or the organization is deactivated. | No. | | `404` | The endpoint, or the resource in your organization, doesn't exist. | No. | | `405` | The endpoint doesn't accept this HTTP method. | No. | | `409` | A conflict: the resource already exists, or it changed since you read it. | For `IDEMPOTENCY_IN_PROGRESS`, after `Retry-After`. For `SEQUENCE_MODIFIED`, after reading the campaign again. | | `413` | The request body is larger than 1 MB. | No. | | `429` | You've hit a rate limit. | Yes, after `Retry-After`. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | `500` | Something failed on our side. | Yes, with backoff. Send an [`Idempotency-Key`](https://docs.versionseven.ai/guides/idempotency) on create requests so a retry is safe. | | `503` | A dependency timed out, or the key couldn't be checked. | Yes, with backoff. | ## Validation errors A request that doesn't match the endpoint's schema answers `400 VALIDATION_ERROR`, and `details.errors` lists every problem. Each entry has: - `field`: the path to the field, with nested fields joined by dots, such as `lead.first_name`. It's `request` when the problem isn't tied to one field. - `message`: what's wrong with the value. - `type`: a short machine-readable reason, such as `missing`. Common causes are a missing required field, a value of the wrong type, a string longer than 2,000 characters, and a number out of range, such as a `limit` above the endpoint's maximum. ## IDs that don't exist - A path ID that isn't a valid UUID answers `404 NOT_FOUND`. - An ID in a request body or query string that isn't a valid UUID, such as `campaign_id` or `stage_id`, answers `400 VALIDATION_ERROR`. - A path ID for a resource that isn't in your organization answers that resource's not-found code, such as `404 CAMPAIGN_NOT_FOUND`. - An ID in a request body that isn't in your organization answers `400`, such as `400 INVALID_STAGE`. This way a response never confirms an ID that belongs to another organization. ## Error codes Every code the API can return. Each endpoint's reference page lists the codes that endpoint can return. | Code | Status | Category | Meaning | | - | - | - | - | | `VALIDATION_ERROR` | 400 | Request | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | `BAD_REQUEST` | 400 | Request | The request was malformed in a way no more specific code covers. | | `NO_UPDATES` | 400 | Request | The update request contained no fields to change. | | `NO_SEQUENCE` | 400 | Request | The campaign has no sequence, so there's no step to update. | | `SEQUENCE_VALIDATION_FAILED` | 400 | Request | The sequence breaks a structural rule, such as an unknown step type or a `linkedin_connection` step without a `conditional` after it. The message lists every rule that failed. | | `INVALID_WEBHOOK_URL` | 400 | Request | The webhook URL must use `https` and resolve to a public address. | | `INVALID_DATE_FILTER` | 400 | Request | `date_filter` must be a number of days such as `30d`, or an ISO 8601 timestamp. | | `INVALID_VARIATION` | 400 | Request | `variation` must be one of `a`, `b`, `unassigned` or `all`. | | `INVALID_LEAD` | 400 | Request | The `lead_id` in the request body isn't a lead in your organization. This is a `400` rather than a `404` so the response never confirms an ID that belongs to another organization. | | `INVALID_STAGE` | 400 | Request | The `stage_id` in the request body isn't a stage in your organization. This is a `400` rather than a `404` for the same reason as `INVALID_LEAD`. | | `INVALID_OWNER` | 400 | Request | The `owner_id` in the request body isn't a member of your organization. This is a `400` rather than a `404` for the same reason as `INVALID_LEAD`. | | `INVALID_ACCOUNT` | 400 | Request | A sender account ID in the request isn't a connected account in your organization. Answered when a connected assistant assigns sender accounts to a campaign. This is a `400` rather than a `404` for the same reason as `INVALID_LEAD`. | | `INVALID_WINDOW` | 400 | Request | The daily-stats window is unparseable, has from after to, or spans more than 400 days. | | `INVALID_GROUP_BY` | 400 | Request | group\_by must be none, sender or variation. | | `PAYLOAD_TOO_LARGE` | 413 | Request | The request body is larger than 1 MB. | | `UNAUTHORIZED` | 401 | Authentication | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | `API_KEY_EXPIRED` | 401 | Authentication | The API key is past its expiry date. Create a new key in the Victoria AI app. | | `FORBIDDEN` | 403 | Authentication | This API key isn't allowed to make the request. | | `INSUFFICIENT_SCOPE` | 403 | Authentication | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | `ORGANIZATION_DEACTIVATED` | 403 | Authentication | The organization that owns this API key has been deactivated. | | `NO_DEFAULT_ORGANIZATION` | 403 | Authentication | You belong to more than one organization and none is set as the default for connected assistants. Pick one on the consent screen, in Settings → Connect your AI, or with the switch\_organization tool. | | `NO_ACTIVE_SUBSCRIPTION` | 403 | Authentication | The organization doesn't have an active subscription. | | `TRIAL_LEAD_CAP_REACHED` | 403 | Authentication | The organization is on a free trial and has used up its lead quota, so the lead wasn't created. `details` carries the `cap`, the number `used` and the `remaining` count. Subscribe in the Victoria AI app to add more. | | `NOT_FOUND` | 404 | Not found | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | `CAMPAIGN_NOT_FOUND` | 404 | Not found | No campaign with this ID exists in your organization. | | `LEAD_NOT_FOUND` | 404 | Not found | No lead with this ID exists in your organization. | | `LEAD_NOT_IN_CAMPAIGN` | 404 | Not found | The lead isn't enrolled in the campaign given by `campaign_id`. | | `DEAL_NOT_FOUND` | 404 | Not found | No deal with this ID exists in your organization. | | `PIPELINE_NOT_FOUND` | 404 | Not found | No pipeline with this ID exists in your organization. | | `WEBHOOK_NOT_FOUND` | 404 | Not found | No webhook with this ID exists on the campaign. | | `STEP_NOT_FOUND` | 404 | Not found | No step with this ID exists in the campaign's sequence, including inside conditional branches. | | `CONVERSATION_NOT_FOUND` | 404 | Not found | No conversation with this ID exists in your organization. | | `AI_FIELD_NOT_FOUND` | 404 | Not found | No AI field with this ID exists on the campaign. | | `METHOD_NOT_ALLOWED` | 405 | Not found | The path exists but doesn't accept this HTTP method. | | `LEAD_ALREADY_EXISTS` | 409 | Conflict | A lead with the same email or LinkedIn URL already exists in your organization. From [Create a lead](https://docs.versionseven.ai/api-reference/leads/create-lead), this means the request named no `campaign_id` to enrol the existing lead in, or raced an identical request. Changing a lead's email or LinkedIn URL to another lead's answers it too. When it's known, `details.existing_lead_id` identifies the existing lead. | | `LEAD_ALREADY_IN_CAMPAIGN` | 409 | Conflict | The lead already exists in your organization and is already enrolled in the campaign given by `campaign_id`, so there's nothing to do. `details` carries the lead's `existing_lead_id`, the `campaign_id` and the enrolment's `sequence_lead_id`. | | `LEAD_SUPPRESSED` | 409 | Conflict | The contact is on your organization's do-not-contact list, so the lead wasn't created, enrolled or updated. `details.suppressed_by` is `email`, `domain` or `linkedin_url`, and `details.value` is the entry that matched. | | `DUPLICATE_WEBHOOK` | 409 | Conflict | An active webhook for this URL already exists on the campaign. `details.webhook_id` identifies it. | | `WEBHOOK_SLOT_TAKEN` | 409 | Conflict | A campaign has one response webhook, and a different URL already holds this campaign's. `details.webhook_id` identifies it. Send `replace: true` to repoint that webhook at the new URL; the previous receiver then stops getting this campaign's events. | | `CAMPAIGN_NOT_READY` | 409 | Conflict | The campaign can't be activated: at least one readiness check is blocking, such as a step missing content, a variable with no personalization field, an unassigned or unauthenticated sender, or no enrolled leads. `details.checks` lists every check with its status. | | `CAMPAIGN_ACTIVATION_WARNINGS` | 409 | Conflict | The campaign's readiness checks passed with warnings only. `details.checks` lists them. Repeat the request with `ack_warnings: true` to activate anyway. | | `SEQUENCE_MODIFIED` | 409 | Conflict | The sequence changed after it was read. Retrieve the campaign again and retry. | | `STEP_ID_AMBIGUOUS` | 409 | Conflict | More than one step in the sequence has this ID, so the update can't tell which one to change. | | `VARIATION_B_MISSING` | 409 | Conflict | The campaign's sequence has no `variation_b` with steps, so there's nothing to test against variation A. Write one with [Replace a campaign's sequence](https://docs.versionseven.ai/api-reference/campaigns/replace-sequence) before turning A/B testing on or promoting a winner. | | `AB_TESTING_DISABLED` | 409 | Conflict | A/B testing is off for this campaign, so there's no test to promote a winner from. Turn it on with [Turn A/B testing on or off](https://docs.versionseven.ai/api-reference/campaigns/set-ab-testing) first. | | `ACCOUNT_NOT_CONNECTED` | 409 | Conflict | A sender account being assigned to the campaign has disconnected. Reconnect it in the Victoria AI app or with a [reconnect link](https://docs.versionseven.ai/api-reference/accounts/create-connect-link) first. `details.account_ids` lists the accounts. | | `ACCOUNT_IN_USE` | 409 | Conflict | A sender account sends for one active campaign at a time, and one being assigned is already used by another active campaign. `details.conflicts` names each account and the campaign holding it. | | `ACCOUNT_LIMIT_REACHED` | 409 | Conflict | Connecting a new sender account would exceed the organization's seats for that platform. `details` carries the `max` and the number `used`. Free a seat, add one in the Victoria AI app, or reconnect an existing account instead. | | `AI_FIELD_EXISTS` | 409 | Conflict | An active AI field with this name already exists on the campaign. Names are compared without regard to case. | | `IDEMPOTENCY_KEY_REUSED` | 409 | Conflict | This `Idempotency-Key` was already used with a different request. Use a new key for each distinct request. | | `IDEMPOTENCY_IN_PROGRESS` | 409 | Conflict | The first request with this `Idempotency-Key` is still running. Retry after the `Retry-After` interval. | | `RATE_LIMITED` | 429 | Rate limits | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | `DAILY_SPEND_LIMIT` | 429 | Rate limits | Lead-database spend from this connection (an AI connector or API key) would pass the organization's rolling 24-hour limit (5,000 credits by default). `details` carry `daily_limit_credits`, `spent_credits`, `remaining_credits` and `call_ceiling_credits`. Ask for fewer leads or try later; the Lead Database page and the in-app Copilot are not limited. | | `INTERNAL_ERROR` | 500 | Server | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | `ORG_HAS_NO_MEMBERS` | 500 | Server | The organization has no members, so the campaign can't be created. Contact support. | | `UPSTREAM_TIMEOUT` | 503 | Server | A service the API depends on timed out. The request is safe to retry. | | `AUTH_UNAVAILABLE` | 503 | Server | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | | `HTTP_ERROR` | 500 | Server | An HTTP error that no more specific code covers. | --- Source: https://docs.versionseven.ai/guides/pagination # Pagination Page through list endpoints with limit and offset, and use has_more to tell when you've reached the last page. Every list endpoint pages the same way. You ask for a page with `limit` and `offset`, and the response says how many items there are in total and whether another page follows. ## Request parameters | Parameter | Meaning | | - | - | | `limit` | How many items to return. Each endpoint has its own default and maximum, listed below. | | `offset` | How many items to skip before this page. Defaults to `0`. | A `limit` outside the endpoint's range, or a negative `offset`, answers `400 VALIDATION_ERROR`. ## Response fields The items are under a key named for the resource, such as `leads` or `deals`. Alongside them, every list response carries: | Field | Meaning | | - | - | | `count` | Items in this page. | | `total` | Items matching the request, across all pages. | | `limit` | The page size that was applied. | | `offset` | Items skipped before this page. | | `has_more` | `true` when another page follows. | ## Page through a list Request pages until `has_more` is `false`, advancing `offset` by `count` each time: **Node.js** ```javascript async function listAllLeads() { const leads = []; let offset = 0; while (true) { const url = new URL("https://api.versionseven.ai/v1/leads"); url.searchParams.set("limit", "100"); url.searchParams.set("offset", String(offset)); const response = await fetch(url, { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}` }, }); if (!response.ok) throw new Error(`Request failed with status ${response.status}`); const page = await response.json(); leads.push(...page.leads); if (!page.has_more) return leads; offset += page.count; } } ``` **Python** ```python import os import requests def list_all_leads(): leads = [] offset = 0 while True: response = requests.get( "https://api.versionseven.ai/v1/leads", headers={"Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}"}, params={"limit": 100, "offset": offset}, ) response.raise_for_status() page = response.json() leads.extend(page["leads"]) if not page["has_more"]: return leads offset += page["count"] ``` ## Limits by endpoint | Endpoint | Items key | Default limit | Maximum limit | | - | - | - | - | | [`GET /v1/accounts`](https://docs.versionseven.ai/api-reference/accounts/list-accounts) | `accounts` | 500 | 500 | | [`GET /v1/leads`](https://docs.versionseven.ai/api-reference/leads/list-leads) | `leads` | 20 | 100 | | [`GET /v1/campaigns`](https://docs.versionseven.ai/api-reference/campaigns/list-campaigns) | `campaigns` | 500 | 500 | | [`GET /v1/campaigns/{campaign_id}/webhooks`](https://docs.versionseven.ai/api-reference/campaigns/list-webhooks) | `webhooks` | 100 | 500 | | [`GET /v1/crm/pipelines`](https://docs.versionseven.ai/api-reference/crm-pipelines/list-pipelines) | `pipelines` | 100 | 500 | | [`GET /v1/crm/deals`](https://docs.versionseven.ai/api-reference/crm-deals/list-deals) | `deals` | 50 | 100 | ## Ordering Results come back in a stable order, with ties broken by `id`, so paging never repeats or skips an item while the underlying data stays the same. Leads, campaigns and deals are listed newest first, so records created while you page push later pages along. If you need an exact snapshot of a large list, collect the IDs first and fetch details afterwards. ## `page` on the leads list [`GET /v1/leads`](https://docs.versionseven.ai/api-reference/leads/list-leads) also accepts `page`, its original way of paging, and returns `page` in the response. Prefer `offset`, which works the same on every list endpoint. When a request sends both, `offset` wins. --- Source: https://docs.versionseven.ai/guides/rate-limits # Rate limits The per-endpoint and overall request limits on the Victoria AI API, the headers that report them, and how to handle a 429. Requests are limited per API key over a one-minute window, and two limits apply at once: | Limit | Requests per minute | | - | - | | Each endpoint | 100 | | All endpoints combined | 600 | Requests without a valid API key are limited by client IP address instead. ## Headers Every response, including a `429`, reports where you stand against the endpoint's limit: | Header | Meaning | | - | - | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | These headers describe the per-endpoint limit only. A request can exceed the 600-a-minute total while `RateLimit-Remaining` is still above zero. ## When you're limited A request over either limit answers `429` with the error code `RATE_LIMITED` and a `Retry-After` header giving the number of seconds to wait. The request wasn't processed, so it's safe to send again once that time has passed. **Node.js** ```javascript async function fetchWithRetry(url, options, attempts = 5) { for (let attempt = 1; ; attempt++) { const response = await fetch(url, options); if (response.status !== 429 || attempt === attempts) return response; const waitSeconds = Number(response.headers.get("Retry-After")) || 2 ** attempt; await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000)); } } ``` **Python** ```python import time import requests def request_with_retry(method, url, attempts=5, **kwargs): for attempt in range(1, attempts + 1): response = requests.request(method, url, **kwargs) if response.status_code != 429 or attempt == attempts: return response time.sleep(float(response.headers.get("Retry-After") or 2**attempt)) ``` ## Staying under the limits - Spread bulk work out instead of sending it in bursts. 100 requests a minute to one endpoint is a little under two a second. - Watch `RateLimit-Remaining` and slow down before it reaches zero. - Request the largest page an endpoint allows when you list records. See [Pagination](https://docs.versionseven.ai/guides/pagination). - Don't poll for replies. [Webhooks](https://docs.versionseven.ai/guides/webhooks) tell you when a prospect replies. --- Source: https://docs.versionseven.ai/guides/idempotency # Idempotency Send an Idempotency-Key header so a retried create request returns the original result instead of doing the work twice. A request that times out looks the same whether it failed or succeeded and lost its response. Retrying a create request blindly can then make a second campaign or deal. An `Idempotency-Key` header makes the retry safe: the API recognizes the repeat and returns the original response instead of doing the work again. ## Endpoints that accept a key - [`POST /v1/leads`](https://docs.versionseven.ai/api-reference/leads/create-lead) Create a lead - [`POST /v1/leads/create`](https://docs.versionseven.ai/api-reference/leads/create-standalone-lead) Create a standalone lead (deprecated) - [`POST /v1/campaigns`](https://docs.versionseven.ai/api-reference/campaigns/create-campaign) Create a campaign - [`POST /v1/crm/deals`](https://docs.versionseven.ai/api-reference/crm-deals/create-deal) Create a deal The header is optional, and only these endpoints use it. ## Sending a key Send any unique string of up to 255 characters. A UUID is ideal: ```bash curl -X POST https://api.versionseven.ai/v1/crm/deals \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 8e0c6d2f-3f7a-4d6e-9b1a-5c2e7f4a9d10" \ -d '{"name": "Acme Corp expansion"}' ``` Create the key once for each operation, and send the same key with every retry of that operation. Keys are scoped to your organization and remembered for at least 24 hours. ## What a repeat returns | Situation | Response | | - | - | | First request with the key | Runs normally. | | Same key, same request | The original status and body, with the header `Idempotent-Replay: true`. The work isn't done again. | | Same key, different request body | `409 IDEMPOTENCY_KEY_REUSED` | | Same key while the first request is still running | `409 IDEMPOTENCY_IN_PROGRESS`, with `Retry-After` | | The first request failed with a `5xx` error | The key isn't used up, so the retry runs normally. | A replay returns the original response even when it was a `4xx` error. To send a corrected request, use a new key. ## Choosing keys - Use a new key for each distinct operation, and the same key for every retry of it. - To make a repeated sync safe, derive the key from your own record, such as your CRM's ID for the deal. Running the same sync twice then can't create the deal twice. - Don't reuse a key for a different request. That answers `409 IDEMPOTENCY_KEY_REUSED`. ## Without a key Leave the header out and nothing changes, but retries aren't protected. A retried [`POST /v1/leads`](https://docs.versionseven.ai/api-reference/leads/create-lead) answers `409 LEAD_ALREADY_IN_CAMPAIGN` if the first attempt went through (or `409 LEAD_ALREADY_EXISTS` when it had no `campaign_id`), while a retried [`POST /v1/campaigns`](https://docs.versionseven.ai/api-reference/campaigns/create-campaign) or [`POST /v1/crm/deals`](https://docs.versionseven.ai/api-reference/crm-deals/create-deal) creates a duplicate. --- Source: https://docs.versionseven.ai/guides/versioning # Versioning and deprecation How the Victoria AI API is versioned, how deprecated endpoints are announced, and where to get the OpenAPI specification. ## Versions The version is part of every path: all endpoints live under `https://api.versionseven.ai/v1`. Build your integration to ignore response fields it doesn't use, so it keeps working as fields are added. ## Deprecated endpoints When an endpoint is replaced, the original keeps working and is marked deprecated in three ways: - The OpenAPI specification sets `deprecated: true` on it. - Its responses carry the header `Deprecation: true`. - Its responses carry a `Link` header naming the replacement, for example `Link: ; rel="successor-version"`. A `Sunset` header will announce the date a deprecated endpoint stops working, once a date is chosen. No endpoint has a sunset date today. | Deprecated endpoint | Use instead | | - | - | | [`POST /v1/leads/create`](https://docs.versionseven.ai/api-reference/leads/create-standalone-lead) | [`POST /v1/leads`](https://docs.versionseven.ai/api-reference/leads/create-lead) | | [`POST /v1/campaigns/{campaign_id}/webhook/activate`](https://docs.versionseven.ai/api-reference/campaigns/activate-webhook) | [`POST /v1/campaigns/{campaign_id}/webhooks`](https://docs.versionseven.ai/api-reference/campaigns/create-webhook) | | [`POST /v1/campaigns/{campaign_id}/webhook/deactivate`](https://docs.versionseven.ai/api-reference/campaigns/deactivate-webhook) | [`DELETE /v1/campaigns/{campaign_id}/webhooks/{webhook_id}`](https://docs.versionseven.ai/api-reference/campaigns/delete-webhook) | | [`GET /v1/campaigns/sequence-templates`](https://docs.versionseven.ai/api-reference/campaigns/list-sequence-templates) | [`GET /v1/sequence-templates`](https://docs.versionseven.ai/api-reference/reference/list-sequence-templates) | | [`GET /v1/campaigns/webhook/examples`](https://docs.versionseven.ai/api-reference/campaigns/list-webhook-examples) | [`GET /v1/webhooks/examples`](https://docs.versionseven.ai/api-reference/reference/list-webhook-examples) | ## OpenAPI specification The full specification is public, and this reference is generated from it: - JSON: - YAML: Use it to generate a client, or to import the API into a tool such as Postman. --- Source: https://docs.versionseven.ai/guides/webhooks # Receiving webhooks Register a webhook on a campaign to hear when a prospect replies, and handle deliveries, retries and duplicates. Instead of polling for replies, register a webhook on a campaign. When a prospect replies, Victoria AI sends a [`prospect_response`](https://docs.versionseven.ai/api-reference/webhooks/prospect-response) event to your URL, with the reply, the lead, the campaign, and the AI Appointment Setter's read of the reply. ## 1. Build an endpoint Your endpoint must: - Accept `POST` requests with a JSON body over **HTTPS**, at an address reachable from the public internet. - Respond with a `2xx` status within 75 seconds. Acknowledge the delivery first, then do slow work such as CRM updates. - Check the `X-Signature-256` header before trusting the body. See [Verifying signatures](https://docs.versionseven.ai/guides/verifying-webhooks). To build and test it before a live campaign sends anything, use the sample deliveries from [`GET /v1/webhooks/examples`](https://docs.versionseven.ai/api-reference/reference/list-webhook-examples). ## 2. Register the webhook Register your URL on a campaign with [`POST /v1/campaigns/{campaign_id}/webhooks`](https://docs.versionseven.ai/api-reference/campaigns/create-webhook). Send a `secret` of at least 16 characters to sign deliveries with a value you already hold, or leave it out and one is generated: ```bash curl -X POST https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/webhooks \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"webhook_url": "https://example.com/victoria/webhooks"}' ``` The response includes the `webhook_id`, and the `secret` when one was generated. > **Warning:** A generated secret is returned only in this response. Store it before you discard the response. If you lose it, set a new one with [`POST /v1/campaigns/{campaign_id}/webhooks/{webhook_id}/secret`](https://docs.versionseven.ai/api-reference/campaigns/rotate-webhook-secret). | Response | When | | - | - | | `201` | The webhook was created. | | `200` | A disabled webhook for the same URL was enabled again. | | `409 DUPLICATE_WEBHOOK` | The URL is already active on this campaign. | | `400 INVALID_WEBHOOK_URL` | The URL isn't HTTPS, or doesn't resolve to a public address. | A campaign can have several webhooks, and each delivery goes to all of them at the same time. A lead's reply is normally delivered once per campaign, not once per message. The exception is described under [Retries and duplicates](https://docs.versionseven.ai/guides/webhooks#retries-and-duplicates). ## 3. Handle deliveries Each delivery is a `POST` with `Content-Type: application/json`. A shortened example: ```json { "event": "prospect_response", "idempotency_key": "f3a1c9d2b8e7460a91d0a25f3e7b4c1d6f8a92e0b4c1d6f8a92e0b4c1d6f8a92", "sequence_lead_id": "550e8400-e29b-41d4-a716-446655440000", "timestamp": "2025-01-14T19:30:00+00:00", "campaign": "Q1 2024 Outbound Campaign", "channel": "email", "prospect_message": "Hi, I'm interested in learning more about your product.", "ai_response": { "sentiment": "positive", "out_of_office": false, "sdr_brief": "Prospect expressed interest, recommend scheduling demo within 24 hours." } } ``` The [event reference](https://docs.versionseven.ai/api-reference/webhooks/prospect-response) documents every field, including the lead, the sending account and the rest of `ai_response`. ## Retries and duplicates - A delivery fails when your endpoint answers with an error status or doesn't respond within 75 seconds. - If every webhook on the campaign fails, the delivery is retried every 15 minutes, up to 5 attempts within 48 hours. - A retry carries the same `idempotency_key` as the original delivery. Record the keys you've processed and skip any you've already seen. - A later reply from the same lead can arrive as a new event with a new `idempotency_key`, for example when a prospect who first asked a question then says yes. ## Managing webhooks | Task | Endpoint | | - | - | | List a campaign's webhooks | [`GET /v1/campaigns/{campaign_id}/webhooks`](https://docs.versionseven.ai/api-reference/campaigns/list-webhooks) | | Stop deliveries to a webhook | [`DELETE /v1/campaigns/{campaign_id}/webhooks/{webhook_id}`](https://docs.versionseven.ai/api-reference/campaigns/delete-webhook) | | Set or rotate the signing secret | [`POST /v1/campaigns/{campaign_id}/webhooks/{webhook_id}/secret`](https://docs.versionseven.ai/api-reference/campaigns/rotate-webhook-secret) | Deleting a webhook disables it rather than destroying it. Registering the same URL again re-enables it, along with its existing signing secret. --- Source: https://docs.versionseven.ai/guides/verifying-webhooks # Verifying webhook signatures Check the X-Signature-256 header on each webhook delivery so your endpoint only acts on requests that came from Victoria AI. Anyone who learns your webhook URL can send a request to it. When a webhook has a signing secret, every delivery carries a signature you can check, so your endpoint only acts on genuine events. ## How deliveries are signed Victoria AI computes an HMAC-SHA256 of the raw request body, keyed with the webhook's secret, and sends the lowercase hex digest in the `X-Signature-256` header, prefixed with `sha256=`. To verify a delivery: 1. Read the raw request body as bytes, before parsing any JSON. 2. Compute the HMAC-SHA256 of those bytes with your secret, as lowercase hex. 3. Compare `sha256=` followed by your digest with the header, using a constant-time comparison. 4. If they don't match, reject the request with `401`. > **Warning:** Verify the exact bytes you received. Parsing the JSON and serializing it again changes spacing and key order, and the signature won't match. ## Examples **Node.js (Express)** ```javascript import crypto from "node:crypto"; import express from "express"; const app = express(); const secret = process.env.VICTORIA_WEBHOOK_SECRET; function isValidSignature(rawBody, header) { if (typeof header !== "string") return false; const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex"); const expectedBytes = Buffer.from(expected); const receivedBytes = Buffer.from(header); return expectedBytes.length === receivedBytes.length && crypto.timingSafeEqual(expectedBytes, receivedBytes); } // express.raw keeps the body as the exact bytes that were signed. app.post("/victoria/webhooks", express.raw({ type: "application/json" }), (req, res) => { if (!isValidSignature(req.body, req.get("X-Signature-256"))) { return res.sendStatus(401); } const event = JSON.parse(req.body.toString("utf8")); res.sendStatus(200); // Your own processing. Skip idempotency keys you've already handled. processEvent(event); }); ``` **Python (Flask)** ```python import hashlib import hmac import os from flask import Flask, abort, request app = Flask(__name__) SECRET = os.environ["VICTORIA_WEBHOOK_SECRET"].encode() def is_valid_signature(raw_body: bytes, header: str | None) -> bool: if not header: return False expected = "sha256=" + hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, header) @app.post("/victoria/webhooks") def victoria_webhook(): raw_body = request.get_data() # the exact bytes that were signed if not is_valid_signature(raw_body, request.headers.get("X-Signature-256")): abort(401) event = request.get_json() process_event(event) # your own processing; skip idempotency keys you've already handled return "", 200 ``` ## Webhooks without a secret A webhook created without a secret receives unsigned deliveries, with no `X-Signature-256` header. Add a secret with [`POST /v1/campaigns/{campaign_id}/webhooks/{webhook_id}/secret`](https://docs.versionseven.ai/api-reference/campaigns/rotate-webhook-secret) first, then turn on verification in your endpoint. ## Rotating a secret Setting a new secret with the same endpoint replaces the old one, and deliveries are signed with the new secret from then on. To rotate without rejecting any deliveries: 1. Choose the new secret yourself, at least 16 characters, and deploy an endpoint that accepts a signature made with either the old or the new secret. 2. Send the new secret to [`POST /v1/campaigns/{campaign_id}/webhooks/{webhook_id}/secret`](https://docs.versionseven.ai/api-reference/campaigns/rotate-webhook-secret). 3. Once deliveries verify against the new secret, remove the old one from your endpoint. ## Replayed requests The signature doesn't cover a timestamp, so a captured delivery would still verify if someone sent it again. Record each `idempotency_key` you process and ignore repeats. That also absorbs Victoria AI's own retries. --- Source: https://docs.versionseven.ai/help # Help Center How to use the Victoria AI app: senders, leads, campaigns, the Appointment Setter, billing and troubleshooting. ## Getting started - [What is Victoria AI](https://docs.versionseven.ai/help/what-is-victoria): What Victoria AI does, what each part of the app is for, and the order most teams set it up in. - [How the free trial works](https://docs.versionseven.ai/help/free-trial): What the Victoria AI free trial includes, what ends it early, and when your card is charged or not. - [Set up Victoria AI with the Copilot](https://docs.versionseven.ai/help/set-up-with-the-copilot): How the guided setup conversation works, how to skip it, and how to pick it up again from the dashboard. - [Setup credits](https://docs.versionseven.ai/help/setup-credits): The bonus credits each setup milestone earns, when each one lands, and the rules that apply to them. - [Your first month with Victoria AI](https://docs.versionseven.ai/help/first-month): A week-by-week routine for your first month with Victoria AI, from setup and daily replies to weekly analytics, A/B decisions and lead refills. - [Victoria AI glossary](https://docs.versionseven.ai/help/glossary): Plain definitions of the terms used across Victoria AI, from backlog and runway to seats, ramps and preflight. ## Senders - [Connect a LinkedIn account](https://docs.versionseven.ai/help/connect-linkedin): Connect your LinkedIn account as a sender in Victoria AI, what happens after it connects, and what to do if it fails. - [Connect a Gmail or Outlook mailbox](https://docs.versionseven.ai/help/connect-gmail-or-outlook): Connect a Google Workspace or Microsoft 365 mailbox as an email sender, and what the DNS check does once it connects. - [Connect a sender with an invite link](https://docs.versionseven.ai/help/connect-a-sender-with-an-invite-link): Send a colleague or client a link that connects their LinkedIn or email account to your workspace without sharing a password. - [Reconnect a disconnected sender](https://docs.versionseven.ai/help/reconnect-a-sender): Why a LinkedIn or email sender disconnects, what stops while it's disconnected, and how to reconnect it from Sender Accounts. - [LinkedIn limits, ramp and restrictions](https://docs.versionseven.ai/help/linkedin-restrictions-and-limits): The daily and weekly caps on each LinkedIn sender, the warm-up ramp, and what happens after LinkedIn restricts an account. - [Sending mailbox best practices](https://docs.versionseven.ai/help/sending-mailbox-best-practices): Set up mailboxes for Victoria AI cold email the safe way, on a dedicated sending domain, warmed up and with SPF, DKIM and DMARC in place. - [Email sending limits and the warm-up ramp](https://docs.versionseven.ai/help/sender-limits-and-ramp): How many emails each mailbox and campaign can send, how the warm-up ramp works, and why Victoria has no separate warm-up service. - [Set up SPF, DKIM and DMARC for your sending domain](https://docs.versionseven.ai/help/dns-setup): What SPF, DKIM and DMARC do, how to read the DNS check on Sender Accounts, and how to add the records for Google or Microsoft. - [Remove or replace a sender](https://docs.versionseven.ai/help/remove-or-replace-a-sender): What removing a LinkedIn or email sender pauses and stops, who can remove one, and how to swap in a different account. ## Leads - [Find leads in the Lead database](https://docs.versionseven.ai/help/find-leads-in-the-lead-database): Search the Lead database from a campaign, preview results, and add leads with emails or LinkedIn only, with what each step costs. - [Import leads from a CSV](https://docs.versionseven.ai/help/import-a-csv): Upload a CSV or XLSX of leads into a campaign, map its columns, and turn extra columns into variables for your sequence. - [Do Not Contact list](https://docs.versionseven.ai/help/do-not-contact): Block emails, domains and LinkedIn profiles from every campaign, what the list blocks, and how opt-outs are added automatically. - [Trial lead cap](https://docs.versionseven.ai/help/trial-lead-cap): How many leads a trial workspace can hold, what counts toward the cap, and what to do when an import is refused. ## Campaigns - [Build a campaign](https://docs.versionseven.ai/help/build-a-campaign): Create a campaign in Victoria AI from start to activation, in the app or with the AI Sales Copilot. - [Sequence steps and branches](https://docs.versionseven.ai/help/sequence-steps-and-branches): Every step type in a Victoria AI sequence, how delays work, and how the connection-accepted branch splits leads. - [Personalization fields and variables](https://docs.versionseven.ai/help/personalization-fields): Lead variables, custom fields and AI Personalization fields in Victoria AI messages, with fallbacks, coverage, held leads and costs. - [A/B test a sequence](https://docs.versionseven.ai/help/ab-testing): Run two variations of a Victoria AI sequence, split new leads between them, read the verdict, and end the test. - [Work hours and daily limits](https://docs.versionseven.ai/help/work-hours-and-daily-limits): Set when a Victoria AI campaign sends and how much it sends each day, and how campaign limits combine with sender limits. - [Preflight checks](https://docs.versionseven.ai/help/preflight-checks): Every readiness check Victoria AI runs before a campaign activates, what each message means, and how to fix it. - [Edit a live campaign](https://docs.versionseven.ai/help/edit-a-live-campaign): Which changes are safe while a Victoria AI campaign has leads in progress, and why adding or deleting steps can shift leads. - [Pause, duplicate or archive a campaign](https://docs.versionseven.ai/help/pause-duplicate-archive): Stop a Victoria AI campaign for now, copy it as a starting point, or archive it for good, and what each one keeps. ## Inbox & Appointment Setter - [Inbox basics](https://docs.versionseven.ai/help/inbox-basics): Read and answer replies in the Victoria AI Inbox, filter conversations, close and reopen them, and see the whole team's threads. - [Appointment Setter](https://docs.versionseven.ai/help/appointment-setter): Set up the AI Appointment Setter on a Victoria AI campaign, choose its mode and goal, and take over or hand back conversations. - [Replies and sentiment](https://docs.versionseven.ai/help/replies-and-sentiment): How Victoria AI labels each reply, what happens to a lead who replies or opts out, and how to correct the record. ## Analytics - [Analytics metrics](https://docs.versionseven.ai/help/analytics-metrics): What every number on the Victoria AI Dashboard and campaign Analytics tab counts, its unit, and how date ranges and filters apply. - [Campaign health indicators](https://docs.versionseven.ai/help/campaign-health): What the warning icons and banners on a Victoria AI campaign mean, and how to clear them. ## Connect your AI - [Connect your AI assistant](https://docs.versionseven.ai/help/connect-your-ai-assistant): Connect Claude or ChatGPT to your Victoria AI workspace in three steps, and choose read-only or read-and-write access. ## Billing - [Plans and add-ons](https://docs.versionseven.ai/help/plans-and-add-ons): What the Workspace and Agency plans include, how to add LinkedIn seats and mailboxes, and how removing an add-on works. - [Credits and rollover](https://docs.versionseven.ai/help/credits-and-rollover): What spends credits in Victoria AI, what is free, how the monthly grant rolls over, and what stops when the balance reaches zero. - [Buy credits and set up auto-recharge](https://docs.versionseven.ai/help/buy-credits-and-auto-recharge): Buy a credit pack from Credits & Billing, set auto-recharge to top up when your balance runs low, and fix a purchase that didn't land. - [Failed payments](https://docs.versionseven.ai/help/failed-payments): What pauses when a subscription payment fails, how long your senders are kept, and how to update your card to resume. - [Cancel or reactivate your subscription](https://docs.versionseven.ai/help/cancel-or-reactivate): How canceling works for a trial and a paid plan, what happens to your senders and credits, and how to reactivate. - [Plans, limits and credits at a glance](https://docs.versionseven.ai/help/facts): Every plan price, credit cost, trial limit, sending cap and setting bound in Victoria AI, on one page. ## Account - [Invite teammates and manage roles](https://docs.versionseven.ai/help/invite-teammates-and-roles): Invite people to your Victoria AI workspace, what the owner, admin and member roles can each do, and how to change or remove someone. - [Create and manage API keys](https://docs.versionseven.ai/help/api-keys): Generate an API key with the access it needs and an expiry, use it with the REST API or MCP, and revoke it when you're done. - [Notifications and alert emails](https://docs.versionseven.ai/help/notifications): Where Victoria AI notifications appear, what they alert you to, who gets the matching emails, and how to clear them. ## Troubleshooting - [Campaign won't activate](https://docs.versionseven.ai/help/troubleshooting-campaign-wont-activate): Why the Activate Campaign button stays disabled in Victoria AI, and how to clear each kind of blocker. - [Leads aren't moving](https://docs.versionseven.ai/help/troubleshooting-leads-not-moving): Why leads in an active Victoria AI campaign aren't getting their next step, and the fix for each cause. - [Campaign gets no replies](https://docs.versionseven.ai/help/troubleshooting-no-replies): Diagnose a Victoria AI campaign that sends but gets few or no replies, from deliverability to copy and targeting. - [Sender disconnected](https://docs.versionseven.ai/help/troubleshooting-sender-disconnected): What happens to a Victoria AI campaign when a LinkedIn or email sender disconnects, and how to get it sending again. --- Source: https://docs.versionseven.ai/help/what-is-victoria # What is Victoria AI What Victoria AI does, what each part of the app is for, and the order most teams set it up in. Victoria AI is an outbound AI SDR. It sends LinkedIn and email sequences from your own accounts, writes each message for the person receiving it, and answers replies until a meeting is booked. You decide who to contact and what to say; Victoria does the sending, the follow-up and the first conversation. ## What Victoria AI does for you - **Sends sequences on LinkedIn and email.** A campaign is a sequence of steps (view profile, connection request, LinkedIn message, email) that runs for every lead you add. See [Build a campaign](https://docs.versionseven.ai/help/build-a-campaign). - **Personalizes each message.** AI Personalization fields write a line for each lead from their website, LinkedIn profile and recent posts. See [Personalization fields](https://docs.versionseven.ai/help/personalization-fields). - **Answers replies.** The AI Appointment Setter reads each reply, answers it and follows up until the prospect books or says no. See [Appointment Setter](https://docs.versionseven.ai/help/appointment-setter). - **Keeps your accounts safe.** Every sender warms up over 14 days and stays inside daily and weekly caps. See [Sender limits and ramp](https://docs.versionseven.ai/help/sender-limits-and-ramp). ## The parts of the Victoria AI app The sidebar holds every page: - **Dashboard**: results across all campaigns. See [Analytics metrics](https://docs.versionseven.ai/help/analytics-metrics). - **Inbox**: every reply from every sender in one place. See [Inbox basics](https://docs.versionseven.ai/help/inbox-basics). - **CRM**: a deal board for prospects who replied. - **Campaigns**: your campaigns, their leads, sequences and settings. - **Sender Accounts**: the LinkedIn and email accounts Victoria sends from. - **Credits & Billing**: your plan, credit balance and payment settings. - **Team**: the people in your workspace and their roles. - **Live Onboarding**: book a call with our team to set up together. - **AI Sales Copilot**: an assistant that can build campaigns, find leads and run checks for you. Talking to it is free. - **Feedback & Support**: send us a message. - **Notifications** and **Settings**: alerts, your profile, API keys and the Do Not Contact list. ## The order to set up Victoria AI in 1. Start the free trial. See [Free trial](https://docs.versionseven.ai/help/free-trial). 2. Follow the guided setup with the Copilot, or skip it and set up by hand. See [Set up with the Copilot](https://docs.versionseven.ai/help/set-up-with-the-copilot). 3. Connect a sender: [LinkedIn](https://docs.versionseven.ai/help/connect-linkedin) or [Gmail or Outlook](https://docs.versionseven.ai/help/connect-gmail-or-outlook). For email, check your domain's [DNS records](https://docs.versionseven.ai/help/dns-setup). 4. Add leads: [find them in the Lead database](https://docs.versionseven.ai/help/find-leads-in-the-lead-database) or [import a CSV](https://docs.versionseven.ai/help/import-a-csv). 5. Write the sequence and activate the campaign. Activation runs the [preflight checks](https://docs.versionseven.ai/help/preflight-checks). Each of these steps also earns setup credits once. See [Setup credits](https://docs.versionseven.ai/help/setup-credits). ## What Victoria AI costs to run You pay for a plan, which includes sender seats and a monthly credit grant, and credits pay for the AI work. See [Plans and add-ons](https://docs.versionseven.ai/help/plans-and-add-ons) and [Credits and rollover](https://docs.versionseven.ai/help/credits-and-rollover). --- Source: https://docs.versionseven.ai/help/free-trial # How the free trial works What the Victoria AI free trial includes, what ends it early, and when your card is charged or not. Every new workspace starts with a free trial of the Workspace plan. A card is required to start it, and nothing is charged that day. ## Start the free trial After you sign up, the **Start your free trial** page opens. Select **Start free trial**, enter your card on the Stripe checkout page, and you return to Victoria AI with the trial running. The trial lasts 30 days. Each workspace gets one trial. If your workspace already used one, the page offers **Subscribe to Workspace** instead, which is a paid checkout. ## What the free trial includes - 1 LinkedIn account and 1 mailbox. - Up to 250 leads in the workspace. See [Trial lead cap](https://docs.versionseven.ai/help/trial-lead-cap). - 400 credits, granted once when the trial starts. - 330 credits of Lead database use (searches and found emails together), if the Lead database is on for your workspace. - Unlimited users and campaigns. Seat and mailbox add-ons are locked until the trial converts to a paid plan. ## What ends the free trial early The trial ends early, and billing starts, as soon as either limit is reached: - **200 sends.** Emails, LinkedIn messages, connection requests and Appointment Setter replies all count. - **20 Appointment Setter conversations.** A conversation counts once the Setter has replied to that lead. Check your progress in **Credits & Billing → Plan**, which shows **Messages sent** and **Setter conversations** against each limit. ## When your card is charged at the end of the free trial At the end of the trial, your card is charged for the Workspace plan only if a sender was ever connected. A sender that is connected now counts, and so does one that disconnected and is waiting to be reconnected. A sender you removed doesn't count. 3 days before the trial ends, Victoria AI sends a notice: - **"Your trial ends in 3 days"** if a sender is connected. Nothing to do. - **"Reconnect your sender before your plan starts"** if your only sender is disconnected. The plan still starts; reconnect so campaigns keep sending. See [Reconnect a sender](https://docs.versionseven.ai/help/reconnect-a-sender). - **"Connect a sender to keep your workspace"** if no sender was ever connected. ## What happens if no sender was connected during the free trial The trial ends with no charge and the workspace closes: campaigns pause, and you can't connect senders or activate campaigns. **Credits & Billing** then shows **Trial ended**. To come back, select **Subscribe to Workspace**; it's a paid checkout, because the trial has been used. To keep the workspace, connect a LinkedIn or email sender before the last day. Connecting one cancels the scheduled close, and the plan starts at the end of the trial as normal. ## Cancel the free trial Canceling a trial ends it immediately, with no charge. See [Cancel or reactivate](https://docs.versionseven.ai/help/cancel-or-reactivate). --- Source: https://docs.versionseven.ai/help/set-up-with-the-copilot # Set up Victoria AI with the Copilot How the guided setup conversation works, how to skip it, and how to pick it up again from the dashboard. After you start the trial, Victoria AI opens **Setup with Victoria**: a conversation with the AI Sales Copilot that takes you to your first live campaign. It asks about your business, builds the campaign with you and checks it before launch. You can skip it at any point and come back later. ## The steps of the guided setup The setup moves through five steps, shown on the right as **Setup progress** (on a phone, tap the progress strip at the top to open it): 1. **Ideal customer**: who you sell to and what you offer. 2. **Leads**: the people to contact, from the Lead database or a CSV upload. 3. **Sequence**: the messages the campaign sends. 4. **Sender account**: the LinkedIn or email account it sends from. 5. **Launch check**: the preflight checks, then activation. When the campaign is live the step list reads **Live** and an **Open campaign** button appears in the header. Each step shows the setup credits it earns. See [Setup credits](https://docs.versionseven.ai/help/setup-credits). ## Work through the guided setup Type in **Reply to Victoria…** and press Enter. When the Copilot wants to change something, such as creating the campaign or writing the sequence, it shows an approval card, and nothing happens until you approve it. Some steps open part of the app inside the conversation: - **Uploading a CSV** opens the same **Upload Leads** window as the campaign's Leads tab. See [Import a CSV](https://docs.versionseven.ai/help/import-a-csv). - **Connecting a sender** takes you to the sign-in page in the same tab, then brings you back to the conversation. See [Connect LinkedIn](https://docs.versionseven.ai/help/connect-linkedin). The campaign stays a draft until you approve activation at the **Launch check** step. ## Skip the guided setup Select **Skip to dashboard** in the header. Nothing you built is lost; the campaign and any leads stay as they are. You can still reach **Sender Accounts**, **Credits & Billing** and **Settings** at any time, whether or not you finished the setup. ## Resume the guided setup after skipping it The dashboard shows **Finish setting up with Victoria** until your first campaign is activated. Select **Continue setup**. The conversation picks up where you left off. The banner goes away on its own once any campaign in the workspace activates, whether you activated it through the setup or by hand. ## Use the Copilot after setup The **AI Sales Copilot** button in the sidebar opens the same assistant in a side panel, so you can keep asking it to build campaigns, find leads or check why a campaign isn't sending. Talking to it doesn't spend credits; only its actions do, such as finding leads. See [Credits and rollover](https://docs.versionseven.ai/help/credits-and-rollover). --- Source: https://docs.versionseven.ai/help/setup-credits # Setup credits The bonus credits each setup milestone earns, when each one lands, and the rules that apply to them. Victoria AI adds bonus credits to your balance as you reach each setup milestone. Each milestone pays once per workspace, 450 credits in all. ## The setup credit milestones and when each lands | Milestone | Credits | When it lands | | - | - | - | | Completed profile | 50 credits | When the workspace owner finishes signing up with their name and company website | | Leads added | 50 credits | When the first lead is added to any campaign | | Sequence built | 50 credits | When a campaign first has a message step with copy in it | | Sender connected | 50 credits | When the first LinkedIn or email sender connects | | Campaign activated | 150 credits | When the first campaign passes the preflight checks and activates (the step plus a launch bonus) | | First reply | 100 credits | When a prospect first replies to a message a campaign sent | The credits land the moment the milestone happens. You don't need to use the guided setup to earn them; a campaign built by hand earns the same credits. ## Where setup credits show up During the guided setup, **Setup progress** lists the credits each step earns and marks them **Credited to your balance** once they land. Your total is in **Credits & Billing** under **Available credits**. ## Rules for setup credits - Each milestone pays once per workspace. Deleting a campaign and building another doesn't pay again. - Setup credits are added to your plan credits, so they follow the same roll-over cap and are forfeited if the subscription ends. See [Credits and rollover](https://docs.versionseven.ai/help/credits-and-rollover). - Setting up email authentication (SPF, DKIM and DMARC) doesn't earn credits. It is checked at activation instead. See [DNS setup](https://docs.versionseven.ai/help/dns-setup). - Only Victoria AI grants these credits. Neither the Copilot nor a connected assistant can grant them. --- Source: https://docs.versionseven.ai/help/first-month # Your first month with Victoria AI A week-by-week routine for your first month with Victoria AI, from setup and daily replies to weekly analytics, A/B decisions and lead refills. Victoria AI does the sending, the follow-up and the first conversation. Your part is a few minutes a day on replies and a short weekly check of the numbers. This is the routine we recommend for the first month. ## Before your first campaign sends Set these up once, before you activate: - **Your LinkedIn profile**: a clear headshot and a short headline about who you help. See [Optimize your LinkedIn profile](https://docs.versionseven.ai/playbook/linkedin-profile). - **Your mailboxes**: on a dedicated sending domain, warmed up with a warm-up service, with SPF, DKIM and DMARC passing. See [Sending mailbox best practices](https://docs.versionseven.ai/help/sending-mailbox-best-practices). - **Your first campaign**: built with the Copilot or from a template, with a blank connection request and give-first messages. See [Set up Victoria AI with the Copilot](https://docs.versionseven.ai/help/set-up-with-the-copilot). - **The Appointment Setter**: a goal with a working booking link, your knowledge base, your own hand-off triggers and a **Notification email** someone watches. Use **Test it** to try it on a few pretend prospects first. See [Configuring the Appointment Setter](https://docs.versionseven.ai/playbook/appointment-setter-configuration). - **A list sized to your senders**: two to three weeks of first touches. See [Size a campaign](https://docs.versionseven.ai/playbook/size-a-campaign). ## Daily habit: answer replies in the Inbox Once a day, open the **Inbox**, or start from the **Dashboard**, whose attention panel links to positive replies waiting for you and conversations handed to you. - Answer every conversation marked **Needs you** the same day. These are the ones the Appointment Setter handed to you, with the reason in a banner. - Read the positive replies the agent is handling, and take over when you'd rather answer yourself. - On a hard no, leave a polite break-up and move on. See [Objections and hard no's](https://docs.versionseven.ai/playbook/objections-and-hard-nos). - When a prospect tells you they booked, select **Mark meeting booked**. Also check **Notifications** for a disconnected sender or a payment problem. A disconnected sender stops its leads until you reconnect it. See [Reconnect a sender](https://docs.versionseven.ai/help/reconnect-a-sender). ## Week 1: let the senders ramp Expect low volume in the first week. Every new sender ramps up over 14 days, starting at 5 LinkedIn requests or messages and 5 emails a day. That's on purpose; don't skip the ramp. - Check that **Contacted** is growing on the campaign's **Analytics** tab. If it isn't, see [Leads aren't moving](https://docs.versionseven.ai/help/troubleshooting-leads-not-moving). - Check each mailbox's DNS chip on **Sender Accounts** reads **DNS ok**. - Don't judge reply rates yet. There isn't enough data. ## Week 2: check acceptance and sender health - **Accept rate** is the first number with enough data to read. If it's low, fix the LinkedIn profile before anything else. - On **Sender Accounts**, check every sender is **Active**, how far through the ramp each one is, and that no domain has slipped to **DNS failing**. - If the sequence uses AI Personalization fields, open **Preview** on a step and check a few results. Add a fallback to any field that comes back **Generic** or held. - Look at **Not yet contacted**. If the backlog will run out within a week, add leads. ## Week 3: read the analytics by bottleneck By now most campaigns have about 100 contacted leads. Once a week, open each campaign's **Analytics** tab and find the bottleneck: the first stage that's below its benchmark, in the order acceptance, replies, positive replies, meetings. Fix that stage and nothing else. - [Outbound benchmarks](https://docs.versionseven.ai/playbook/benchmarks) has the ranges. - [Diagnose a campaign by its bottleneck](https://docs.versionseven.ai/playbook/diagnose-a-campaign) has the checklist. If the copy is the bottleneck, test the fix as Variation B with [A/B testing](https://docs.versionseven.ai/help/ab-testing) rather than replacing what's running. ## Week 4: make A/B decisions and plan the next month - **A/B tests**: read the **A/B verdict** card. Act only on **A wins** or **B wins**, once each arm has at least 50 contacted leads, and ideally 100 or more. **Leaning** means keep it running. To promote the winner, ask the Copilot. - **Refill**: top up each campaign so it has at least a week or two of backlog. - **Capacity**: if a campaign is working and you want more of it, add senders rather than raising limits. - **Trial**: if you're on the free trial, check its progress in **Credits & Billing → Plan**. See [Free trial](https://docs.versionseven.ai/help/free-trial). ## The weekly campaign review Every Monday, Victoria reviews each active campaign that has had activity in the last 14 days, the way the bottleneck checklist does, and proposes up to three changes per campaign, such as a copy rewrite, a Variation B to test, promoting an A/B winner, a lead top-up, or a fix to a personalization field. Nothing changes until someone approves it. - You get a notification, and the workspace owner gets an email, when a review is ready. Open it from either, or from **Reviews** in the Copilot's history. - Each proposal is a card in the Copilot with the evidence, what changes and any credit cost. Approve or skip each one. - Senders that need reconnecting and domains failing their DNS check are listed as findings, with the fix, for you to do. - The review spends up to 10 credits per active campaign each week, and runs only while your balance is above 50 credits. Owners and admins can turn it on or off in **Settings → Weekly campaign review**. Make reading it part of your Monday. ## The routine at a glance - **Daily**: the **Inbox**, starting with **Needs you**; **Notifications** for disconnected senders. - **Weekly**: the weekly campaign review; each campaign's bottleneck on **Analytics**; sender health and DNS on **Sender Accounts**; refill any campaign with less than a week or two of backlog. - **When an A/B test reaches its floor**: decide, promote, and start the next test. --- Source: https://docs.versionseven.ai/help/glossary # Victoria AI glossary Plain definitions of the terms used across Victoria AI, from backlog and runway to seats, ramps and preflight. The terms you'll see in the Victoria AI app, the Copilot and these articles. ## Glossary: campaigns and leads - **Campaign**: a sequence, the leads it runs for, and the senders that send it. See [Build a campaign](https://docs.versionseven.ai/help/build-a-campaign). - **Sequence**: the ordered steps a campaign runs for each lead: view profile, connection request, LinkedIn message and email, with waits between them. See [Sequence steps and branches](https://docs.versionseven.ai/help/sequence-steps-and-branches). - **Variation**: one version of a sequence in an A/B test, **A** or **B**. Each lead is assigned to one. See [A/B testing](https://docs.versionseven.ai/help/ab-testing). - **Backlog**: leads in a campaign that haven't started the sequence yet. They start as the campaign's daily limits allow. - **Runway**: how many days the backlog lasts at the campaign's current daily limits. The Copilot and a connected assistant report it when you ask how many leads a campaign has left. - **Preflight**: the checks a campaign must pass before it activates, such as a sequence with content, leads enrolled and senders assigned. See [Preflight checks](https://docs.versionseven.ai/help/preflight-checks). - **Do Not Contact**: your workspace's list of emails, domains and LinkedIn profiles no campaign may message. See [Do Not Contact](https://docs.versionseven.ai/help/do-not-contact). ## Glossary: senders - **Sender**: a LinkedIn or email account connected on **Sender Accounts** that campaigns send from. - **Seat**: one LinkedIn sender your plan allows. "LinkedIn seat" on the Billing page. - **Mailbox**: one email sender your plan allows, Gmail or Outlook. - **Ramp**: the warm-up period for a newly connected sender, 14 days, during which its daily cap climbs in steps. See [Sender limits and ramp](https://docs.versionseven.ai/help/sender-limits-and-ramp). - **Restriction**: LinkedIn limiting an account. Victoria AI halves that account's LinkedIn daily caps for 7 days after one. See [LinkedIn restrictions and limits](https://docs.versionseven.ai/help/linkedin-restrictions-and-limits). - **SPF, DKIM and DMARC**: the DNS records that prove your mailbox may send for your domain. See [DNS setup](https://docs.versionseven.ai/help/dns-setup). ## Glossary: AI and billing - **Credit**: the unit the AI work is paid in: personalization, the Appointment Setter and the Lead database. See [Credits and rollover](https://docs.versionseven.ai/help/credits-and-rollover). - **Milestone**: a setup step that earns bonus credits once, such as connecting your first sender. See [Setup credits](https://docs.versionseven.ai/help/setup-credits). - **Appointment Setter**: the AI agent that answers replies and follows up until a meeting is booked. See [Appointment Setter](https://docs.versionseven.ai/help/appointment-setter). - **AI Sales Copilot**: the assistant in the sidebar that builds campaigns, finds leads and runs checks with your approval. Talking to it is free. - **AI Personalization field**: a variable the AI writes for each lead at send time. See [Personalization fields](https://docs.versionseven.ai/help/personalization-fields). --- Source: https://docs.versionseven.ai/help/connect-linkedin # Connect a LinkedIn account Connect your LinkedIn account as a sender in Victoria AI, what happens after it connects, and what to do if it fails. A LinkedIn sender sends a campaign's profile views, connection requests and LinkedIn messages. Each one uses a LinkedIn seat on your plan. ## Connect a LinkedIn account 1. Go to **Sender Accounts**. Under **LinkedIn Accounts**, select **Add**. 2. The **Connect Account** window opens on the **LinkedIn** tab. Enter **Your name**: the name the AI uses when it writes as you. 3. Select **Open LinkedIn Sign-in**. The sign-in page opens in the same tab. 4. Sign in to LinkedIn and approve any security check LinkedIn asks for. 5. You return to **Sender Accounts** with the message **Account connected**. The account appears under **LinkedIn Accounts** marked **Active**. If the account doesn't appear right away, refresh the page after a few seconds. To connect someone else's LinkedIn account, such as a colleague's, send them an invite link instead. See [Connect a sender with an invite link](https://docs.versionseven.ai/help/connect-a-sender-with-an-invite-link). ## What happens after a LinkedIn account connects - The account starts its 14 days warm-up ramp, with a low daily cap that rises in steps. The row shows **Day 1 of 14** and today's cap while it ramps. See [LinkedIn restrictions and limits](https://docs.versionseven.ai/help/linkedin-restrictions-and-limits). - The chip next to the account shows its risk level, such as **Risk: elevated** while it ramps. Select the chip to see its risk and sending limits. - To send from it, assign it to a campaign in **Campaigns → your campaign → Settings → Connected Accounts**. Your first connected sender also earns setup credits. See [Setup credits](https://docs.versionseven.ai/help/setup-credits). ## LinkedIn Add button is disabled The **Add** button is disabled in two cases. Hover over it to see which: - **You've reached your LinkedIn seat limit.** The count next to the button, such as **1 of 1 connected**, is full. A pending invite link takes a seat too. On a paid Workspace plan, add a LinkedIn seat in **Credits & Billing**. On Agency, contact support to change seats. During the trial, the trial's LinkedIn seat is the limit until it converts. See [Plans and add-ons](https://docs.versionseven.ai/help/plans-and-add-ons). - **Your subscription is canceled.** Reactivate in **Credits & Billing** to add accounts. See [Cancel or reactivate](https://docs.versionseven.ai/help/cancel-or-reactivate). ## LinkedIn connection fails or never completes If you close the sign-in page or LinkedIn's security check isn't finished, nothing is connected and the seat stays free. Go back to **Sender Accounts** and select **Add** again. If the window shows an error, select **Try again**. If it keeps failing, send us the error text from **Feedback & Support** in the sidebar. --- Source: https://docs.versionseven.ai/help/connect-gmail-or-outlook # Connect a Gmail or Outlook mailbox Connect a Google Workspace or Microsoft 365 mailbox as an email sender, and what the DNS check does once it connects. An email sender sends a campaign's email steps from your own mailbox. Each one uses a mailbox on your plan. Victoria AI connects Gmail (Google Workspace) and Outlook (Microsoft 365) mailboxes. ## Connect a Gmail or Outlook mailbox 1. Go to **Sender Accounts**. Under **Email Accounts**, select **Add**. 2. The **Connect Account** window opens. Choose the **Gmail** tab for Google or the **Outlook** tab for Microsoft. 3. Enter **Your name**: the name the AI uses when it writes as you. 4. Select **Open Gmail Sign-in** or **Open Outlook Sign-in**. The sign-in page opens in the same tab. 5. Sign in with the mailbox you want to send from and approve the access it asks for. 6. You return to **Sender Accounts** with the message **Account connected**. The mailbox appears under **Email Accounts** marked **Active**. If the mailbox doesn't appear right away, refresh the page after a few seconds. To connect a colleague's mailbox, send them an invite link instead. See [Connect a sender with an invite link](https://docs.versionseven.ai/help/connect-a-sender-with-an-invite-link). ## What happens after a mailbox connects - **The DNS check runs.** Victoria AI checks the SPF, DKIM and DMARC records of the mailbox's domain and shows the result on the row: **DNS ok**, **DNS warnings**, **DNS failing** or **DNS unchecked**. A failing domain blocks campaign activation. See [DNS setup](https://docs.versionseven.ai/help/dns-setup). - **The warm-up ramp starts.** The mailbox's daily cap starts low and rises over 14 days. See [Sender limits and ramp](https://docs.versionseven.ai/help/sender-limits-and-ramp). - **Assign it to a campaign** in **Campaigns → your campaign → Settings → Connected Accounts** to send from it. ## Use a company mailbox, not a personal Gmail address Connect a mailbox on your company's domain. A personal address such as an `@gmail.com` mailbox gets the **DNS warnings** chip with "is a personal mailbox domain": it can't carry your own SPF, DKIM or DMARC, and outreach from it has lower deliverability. It isn't blocked, but a Google Workspace or Microsoft 365 mailbox on your domain is the fix. Better still, send from mailboxes on a dedicated sending domain rather than the mailbox you use every day. See [Sending mailbox best practices](https://docs.versionseven.ai/help/sending-mailbox-best-practices). ## Connect an IMAP or SMTP mailbox IMAP and SMTP mailboxes can't be connected yet. The **IMAP** tab in the **Connect Account** window shows **IMAP Coming Soon**. Use a Gmail or Outlook mailbox for now. ## Email Add button is disabled Hover over **Add** to see why: - **You've reached your mailbox limit.** The count, such as **1 of 1 connected**, is full; a pending invite link counts too. On a paid Workspace plan, add a mailbox in **Credits & Billing**. On Agency, contact support. During the trial, the trial's mailbox is the limit until it converts. See [Plans and add-ons](https://docs.versionseven.ai/help/plans-and-add-ons). - **Your subscription is canceled.** Reactivate in **Credits & Billing**. See [Cancel or reactivate](https://docs.versionseven.ai/help/cancel-or-reactivate). --- Source: https://docs.versionseven.ai/help/connect-a-sender-with-an-invite-link # Connect a sender with an invite link Send a colleague or client a link that connects their LinkedIn or email account to your workspace without sharing a password. An invite link lets someone else connect their own LinkedIn, Gmail or Outlook account to your workspace. They sign in on their side; you never see their password. They don't need a Victoria AI login. ## Create a sender invite link 1. Go to **Sender Accounts**. In **LinkedIn Accounts** or **Email Accounts**, select **Invite**. 2. The **Generate Invite Link** window opens. Choose the tab for the account type: **LinkedIn**, **Gmail** or **Outlook**. 3. Enter the **Display name**: the name the AI uses when it writes as this sender. 4. Select **Generate Link**, then **Copy**. 5. Send the link to the person whose account it is. The link expires in 7 days. Anyone who opens it can connect an account, so send it only to the person it's for. ## What happens while a sender invite link is pending The link appears on **Sender Accounts** as a row marked **Pending** with **Invite link active**. A pending link takes a seat or mailbox, the same as a connected account, so the count reads one higher. When the person signs in, the row becomes a normal **Active** account and starts its warm-up ramp. ## Copy or revoke a sender invite link - **Copy it again**: select the copy icon on the pending row. - **Revoke it**: select the trash icon on the pending row, then **Revoke**. The seat is freed. Anyone who already has the link may still be able to use it for a short time. ## Sender invite link button is disabled **Invite** is disabled for the same reasons as **Add**: the seat or mailbox limit is full, or the subscription is canceled. Hover over it to see which. Free a slot by revoking a pending link or removing an account, or add a seat or mailbox in **Credits & Billing**. See [Plans and add-ons](https://docs.versionseven.ai/help/plans-and-add-ons). --- Source: https://docs.versionseven.ai/help/reconnect-a-sender # Reconnect a disconnected sender Why a LinkedIn or email sender disconnects, what stops while it's disconnected, and how to reconnect it from Sender Accounts. A sender disconnects when Victoria AI can no longer sign in to it. Its campaigns keep it assigned, but nothing sends from it until you reconnect. ## What the Sender disconnected warning means When a sender disconnects: - Its row on **Sender Accounts** turns red, reads **Needs reauthentication** and carries a **Disconnected** badge. - A **Sender disconnected** notification appears for the workspace, and the workspace owner gets an email: "needs to be reconnected". - Sends from that account are on hold. Other senders in the same campaign keep sending. - A campaign that uses it can't be activated until it's reconnected; preflight fails on **Accounts within subscription limit**. See [Preflight checks](https://docs.versionseven.ai/help/preflight-checks). ## Why a sender disconnects The usual causes: - **A password change** on the LinkedIn, Google or Microsoft account. - **A login from a new device or a security check.** LinkedIn sometimes asks for a verification code (a checkpoint) before it lets a session continue. - **Expired credentials.** LinkedIn, Google or Microsoft stopped accepting the saved sign-in. - **LinkedIn restricted or deactivated the account.** The chip on the row then shows **Risk: high** with the reason, for example "Restricted 2 days ago". See [LinkedIn restrictions and limits](https://docs.versionseven.ai/help/linkedin-restrictions-and-limits). ## Reconnect a sender 1. Go to **Sender Accounts** and find the row marked **Disconnected**. 2. Select **Reconnect**. The sign-in page opens in the same tab. 3. Sign in to the same account and complete any security check. 4. You return to **Sender Accounts** with the message **Account reconnected**. Reconnecting keeps the same sender: it stays assigned to its campaigns, keeps its leads, and doesn't use another seat. It also doesn't restart the warm-up ramp. The **Sender disconnected** notification is marked read on its own. ## Reconnect fails or the sender disconnects again - **"Failed to start reconnect"**: try again in a minute. If it keeps failing, send the error text from **Feedback & Support**. - **Sign in with the same account.** Signing in with a different LinkedIn or email account doesn't fix the disconnected one. - **It disconnects again soon after**: LinkedIn may be asking for a security check again, or the account is restricted. Sign in to LinkedIn directly, clear any prompt it shows, then select **Reconnect** again. - **You no longer use that account**: remove it instead and connect the new one. See [Remove or replace a sender](https://docs.versionseven.ai/help/remove-or-replace-a-sender). ## Disconnected sender at the end of a trial A sender that disconnected still counts as connected for the trial's charge rule, so the plan starts at the end of the trial. Reconnect it so campaigns keep sending. See [Free trial](https://docs.versionseven.ai/help/free-trial). --- Source: https://docs.versionseven.ai/help/linkedin-restrictions-and-limits # LinkedIn limits, ramp and restrictions The daily and weekly caps on each LinkedIn sender, the warm-up ramp, and what happens after LinkedIn restricts an account. Victoria AI caps every LinkedIn sender so it stays inside the range LinkedIn tolerates on most accounts. The caps apply per account, across every campaign the account sends for, and a campaign's own daily limits apply on top. ## LinkedIn daily and weekly caps per account | Action | Cap | | - | - | | Connection requests and LinkedIn messages, per day | 20 | | Connection requests, per week | 100 | | LinkedIn messages, per week | 200 | | Profile views, per week | 200 | Days are counted in US Eastern time, the same day boundary the campaign daily limits reset on. Connection notes are limited by LinkedIn to 300 characters. ## LinkedIn warm-up ramp for a new account A newly connected LinkedIn account doesn't start at the full cap. Over its first 14 days, connection requests and messages are capped at: | Ramp day, as the app shows it | Per day | | - | - | | Days 1 to 3 | 5 | | Days 4 to 7 | 10 | | Days 8 to 14 | 15 | | After day 14 | The daily cap above | Day 1 is the day the account connected. Profile views aren't ramped. Reconnecting an account doesn't restart its ramp. While it ramps, the row on **Sender Accounts** shows the day and today's cap, for example **Day 4 of 14 · 10/day**. ## What a LinkedIn restriction is and what Victoria does after one A restriction is LinkedIn limiting what an account can do, often after too many invitations or a security flag. When Victoria AI sees one (or the account's sign-in failing, or the account being deactivated), it: - Halves the account's daily caps for connection requests and messages, rounded up, for 7 days. The sending limits read "halved after a restriction". - Marks the account **Risk: high** with the reason and when it happened. After 7 days without another incident, the caps return to normal. If the restriction disconnected the account, reconnect it. See [Reconnect a sender](https://docs.versionseven.ai/help/reconnect-a-sender). ## LinkedIn risk levels on Sender Accounts Select the chip on a LinkedIn row to see its risk: - **Risk: ok**: no incidents in the last 7 days. - **Risk: elevated**: the account is still ramping, or LinkedIn rate-limited it in the last 7 days. - **Risk: high**: a restriction in the last 7 days. Caps are halved. ## Skip the LinkedIn ramp or raise the limits Owners and admins see two buttons in the chip's **Sending limits** section: - **Skip ramp** moves the account straight to its full cap. Accounts that send at full volume immediately are far more likely to be rate-limited or restricted. - **Allow 30/day, 200/week** raises the account to 30 connection requests and messages a day and 200 connection requests a week. This raises the chance of a temporary restriction, especially on accounts under a year old or without Sales Navigator. Both are recorded with your name and can't be undone in the app. Campaign daily limits still apply on top. See [Work hours and daily limits](https://docs.versionseven.ai/help/work-hours-and-daily-limits). --- Source: https://docs.versionseven.ai/help/sending-mailbox-best-practices # Sending mailbox best practices Set up mailboxes for Victoria AI cold email the safe way, on a dedicated sending domain, warmed up and with SPF, DKIM and DMARC in place. How you set up the mailboxes a campaign sends from decides how much of your email reaches the inbox. Do it before you connect them to Victoria AI. ## Don't send cold email from your day-to-day mailbox Don't connect the personal or company mailbox you use for everyday work. Cold email carries more risk than normal email: some recipients mark it as spam, and mailbox providers judge the whole domain by those reports. If your main domain's reputation drops, your invoices, support replies and team email can start landing in spam too. Keep everyday email and outreach apart: outreach goes from dedicated mailboxes on a dedicated sending domain. ## Use a dedicated sending domain Register a second domain close to your brand, such as `getacme.com` or `acmehq.com` for Acme, and use it only for outreach: - Point the domain's website at your main site, so anyone who visits lands somewhere real. - Create mailboxes on it with Google Workspace or Microsoft 365, under real names, such as `jordan@getacme.com`. Victoria AI connects Gmail and Outlook mailboxes; see [Connect a Gmail or Outlook mailbox](https://docs.versionseven.ai/help/connect-gmail-or-outlook). - Don't use a free address such as `@gmail.com`. It can't carry your own DNS records, and the DNS check flags it as a personal mailbox domain. Each connected mailbox uses one mailbox on your plan. Workspace includes 3. See [Plans and add-ons](https://docs.versionseven.ai/help/plans-and-add-ons). ## Warm up new mailboxes with a warm-up service A new mailbox has no sending history, and providers are wary of one that suddenly sends to strangers. A warm-up service builds that history by exchanging email with other mailboxes in its network and marking it as wanted. Victoria AI doesn't warm up mailboxes. It has no warm-up network. What it does is ramp volume: for its first 14 days, a newly connected mailbox sends a small daily number that rises over time, then its normal limits apply. See [Email sending limits and the warm-up ramp](https://docs.versionseven.ai/help/sender-limits-and-ramp). So use a third-party warm-up service for each new sending mailbox: - Start it before the mailbox sends any campaign email. Two to four weeks of warm-up is common. - Keep it running while the mailbox sends campaigns. - Victoria's ramp and caps count only the emails Victoria sends, not the warm-up service's. ## Set up SPF, DKIM and DMARC on the sending domain The sending domain needs all three records before mailbox providers trust it. Victoria AI checks them for every connected mailbox, every day, and won't activate a campaign whose sending domain fails. Once SPF and DKIM pass, move DMARC from `p=none` to `p=quarantine`. See [DNS setup](https://docs.versionseven.ai/help/dns-setup) for the exact records for Google and Microsoft. ## Keep each sending mailbox's volume modest Spread volume across mailboxes rather than pushing one hard: - Each mailbox sends at most 500 emails a week, and each campaign sends up to its **Emails** daily limit per mailbox, 30 by default. - To send more, add mailboxes rather than raising one mailbox's limits. - Don't select **Skip ramp** on a new mailbox. New mailboxes that send at full volume straight away are far more likely to be rate-limited. - Use verified emails. A mailbox whose recent emails bounce too often has its sequence emails paused until the rate drops. ## Sending mailbox setup checklist 1. Register a dedicated sending domain and point it at your main website. 2. Create Google Workspace or Microsoft 365 mailboxes on it, under real names. 3. Add SPF, DKIM and DMARC for the domain. See [DNS setup](https://docs.versionseven.ai/help/dns-setup). 4. Start a third-party warm-up service on each mailbox. 5. Connect each mailbox on **Sender Accounts** and check its chip reads **DNS ok**. 6. Let the ramp run, and assign the mailboxes to a campaign in **Settings → Connected Accounts**. For how this affects copy and reply rates, see [Cold email principles](https://docs.versionseven.ai/playbook/cold-email-principles). --- Source: https://docs.versionseven.ai/help/sender-limits-and-ramp # Email sending limits and the warm-up ramp How many emails each mailbox and campaign can send, how the warm-up ramp works, and why Victoria has no separate warm-up service. Victoria AI limits how fast each mailbox sends, so a new mailbox builds a sending history before it sends at volume. Three limits apply, and the lowest one wins on any day. ## Email limits per campaign and per mailbox | Limit | Default | | - | - | | Emails per campaign, per day | 30 | | Emails per mailbox, per week | 500 | | Emails per mailbox, per day, while ramping | See the ramp below | The per-campaign daily limit is a campaign setting; change it in **Campaigns → your campaign → Settings → Daily Limits**. See [Work hours and daily limits](https://docs.versionseven.ai/help/work-hours-and-daily-limits). The weekly limit per mailbox is fixed. Days are counted in US Eastern time. ## Email warm-up ramp for a new mailbox For its first 14 days a new mailbox has a daily cap across all its campaigns: | Ramp day, as the app shows it | Emails per day | | - | - | | Days 1 to 3 | 5 | | Days 4 to 7 | 10 | | Days 8 to 14 | 20 | | After day 14 | The campaign's daily limit | Day 1 is the day the mailbox connected. While it ramps, the row on **Sender Accounts** shows the day and today's cap, for example **Day 2 of 14 · 5/day**. Reconnecting a mailbox doesn't restart its ramp. ## Skip the email ramp Owners and admins can select the chip on the mailbox's row, then **Skip ramp** under **Sending limits**. The mailbox moves straight to the campaign's daily limit. New and recently reconnected mailboxes that send at full volume immediately are far more likely to be rate-limited by the mailbox provider. The skip is recorded with your name and can't be undone. ## Why Victoria AI has no mailbox warm-up service Victoria AI doesn't run a warm-up network that sends and replies between mailboxes. The protection is the ramp and the caps above, plus checks on your domain: - SPF, DKIM and DMARC are checked when a mailbox connects and every day after. A failing domain blocks campaign activation. See [DNS setup](https://docs.versionseven.ai/help/dns-setup). - Every message is written for one person rather than sent as a blast. For a new mailbox, use a third-party warm-up service as well, on a dedicated sending domain rather than your everyday mailbox. See [Sending mailbox best practices](https://docs.versionseven.ai/help/sending-mailbox-best-practices). ## Campaign sends fewer emails than the limit A campaign can send less than its daily limit when: - The mailbox is still ramping. Check the day count on **Sender Accounts**. - The mailbox reached its weekly limit. - The campaign is outside its work hours. - Few leads are due an email step today. See [Leads not moving](https://docs.versionseven.ai/help/troubleshooting-leads-not-moving) for the full list. --- Source: https://docs.versionseven.ai/help/dns-setup # Set up SPF, DKIM and DMARC for your sending domain What SPF, DKIM and DMARC do, how to read the DNS check on Sender Accounts, and how to add the records for Google or Microsoft. Mailbox providers check three DNS records before they trust email from your domain. Victoria AI checks them for every connected mailbox and won't activate a campaign whose sending domain fails. ## What SPF, DKIM and DMARC are - **SPF** is a TXT record on your domain that lists the servers allowed to send its email. Your SPF must include Google's or Microsoft's servers, because Victoria AI sends through your mailbox provider. - **DKIM** is a key your mailbox provider uses to sign each email, published as a DNS record. Receivers check the signature against it. - **DMARC** is a TXT record at `_dmarc` that tells receivers what to do with mail that fails SPF and DKIM, and where to send reports. You add all three where your domain's DNS is managed, which is usually the registrar you bought the domain from. ## Read the DNS check on Sender Accounts Each email row on **Sender Accounts** has a DNS chip. Select it to see the **Authentication** section: the domain, where its DNS is managed, when it was last checked, and a line for SPF, DKIM and DMARC. | Chip | Meaning | Blocks activation | | - | - | - | | **DNS ok** | All three records pass. | No | | **DNS warnings** | Something to improve: DKIM not found at a common selector, DMARC set to `p=none`, no MX records, or a personal mailbox domain. | No | | **DNS failing** | No SPF record, more than one SPF record, SPF missing Google's or Microsoft's include, or no DMARC record. | Yes | | **DNS unchecked** | The check couldn't reach DNS, or hasn't run yet. It retries daily. | No | A DKIM warning can be ignored if your DKIM already uses a custom selector name. Every issue in the popover comes with the exact fix, including the record value for your domain. The check runs when a mailbox connects and every day after. After you change DNS, select **Re-check DNS**; changes can take up to an hour to show. When the DNS host is recognized, the popover names it (for example GoDaddy or Cloudflare) with a **How to add a record** link to that host's own instructions. ## DNS records for Google Workspace mailboxes Add these at your DNS host. Replace `yourdomain.com` with your domain. | Record | Type | Host / Name | Value | | - | - | - | - | | SPF | TXT | `@` | `v=spf1 include:_spf.google.com ~all` | | DKIM | TXT | `google._domainkey` | The key Google generates (see below) | | DMARC | TXT | `_dmarc` | `v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com` | To get the DKIM key: in Google Admin, go to **Apps → Google Workspace → Gmail → Authenticate email**, generate a key for your domain, add the TXT record it shows, then select **Start authentication**. ## DNS records for Microsoft 365 mailboxes | Record | Type | Host / Name | Value | | - | - | - | - | | SPF | TXT | `@` | `v=spf1 include:spf.protection.outlook.com -all` | | DKIM | CNAME | `selector1._domainkey` | The value Microsoft shows | | DKIM | CNAME | `selector2._domainkey` | The value Microsoft shows | | DMARC | TXT | `_dmarc` | `v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com` | To get the DKIM values: in Microsoft 365 Defender, go to **Email & collaboration → Policies & rules → Threat policies → DKIM**, select your domain, create the two CNAME records it shows, then turn on signing. ## Rules for the SPF record - **Keep one SPF record.** A domain with two TXT records that start with `v=spf1` fails, the same as having none. If you already have one, add the include to it rather than creating a second. - **Put the include before the ending.** For example, `v=spf1 include:_spf.google.com include:other.example ~all`. - **If your mail goes through a gateway** such as Mimecast or Proofpoint, still add Google's or Microsoft's include. Victoria AI sends through your mailbox provider directly. ## Move DMARC from p=none to enforcement `p=none` only monitors. It passes the check with a warning, because mailbox providers trust domains that enforce a policy more. Once SPF and DKIM pass for everything that sends as your domain, change `p=none` to `p=quarantine` in the `_dmarc` record, and later to `p=reject`. ## Add DNS records at GoDaddy Sign in to GoDaddy, open your domain portfolio, select the domain, and open its **DNS** settings. Add a record, choose the type (TXT or CNAME), enter the host from the tables above (`@` for the domain itself), paste the value, and save. To edit an existing SPF record, edit that TXT record instead of adding a new one. ## Add DNS records at Cloudflare In the Cloudflare dashboard, select the domain, then **DNS → Records → Add record**. Choose the type, enter the name (`@` for the domain itself) and the content, and save. For the Microsoft DKIM CNAME records, set the proxy status to **DNS only**, because a proxied CNAME won't return the key. ## Add DNS records at Namecheap In your Namecheap account, open **Domain List**, select **Manage** next to the domain, then the **Advanced DNS** tab. Under host records, add a new record, choose the type, enter the host (`@` for the domain itself, or `_dmarc`, `google._domainkey` and so on, without your domain name on the end) and the value, and save. ## Add DNS records at Squarespace or Google Domains Domains that were on Google Domains are now managed at Squarespace. In Squarespace, open **Domains**, select the domain, and open its **DNS** settings. Add a custom record, choose the type, enter the host and the value, and save. If the domain's DNS is managed somewhere else (for example its nameservers point to Cloudflare), add the records there instead; the DNS check names the right host. ## Sender domain still failing after adding records - **Wait, then select Re-check DNS.** DNS changes can take up to an hour to show. - **Check the host name.** Many DNS hosts add your domain automatically, so `_dmarc.yourdomain.com` typed in full can become `_dmarc.yourdomain.com.yourdomain.com`. Enter only `_dmarc`. - **Check you edited the right place.** The popover says where DNS for your domain is managed. Records added anywhere else have no effect. - **Check for a second SPF record.** Merge them into one. When the chip reads **DNS ok**, the campaign's **Email sender domains authenticated (SPF, DKIM, DMARC)** check passes. **DNS warnings** and **DNS unchecked** show as a warning there but don't block activation. See [Preflight checks](https://docs.versionseven.ai/help/preflight-checks). --- Source: https://docs.versionseven.ai/help/remove-or-replace-a-sender # Remove or replace a sender What removing a LinkedIn or email sender pauses and stops, who can remove one, and how to swap in a different account. Removing a sender disconnects it from Victoria AI and takes it off every campaign. Do it when you no longer want to send from that account, or to free its slot for a different one. ## Remove a sender 1. Go to **Sender Accounts** and find the account. 2. Select the trash icon on its row. 3. Read the **Remove sender account?** dialog, then select **Remove**. The toast tells you what changed, for example "Paused 1 campaign (Q4 outreach) and stopped 12 leads assigned to this sender". ## What removing a sender pauses and stops - **It's taken off every campaign's sender list.** - **Active campaigns that used it pause.** This happens even if the campaign has other senders. - **Leads assigned to it stop.** They're marked disconnected and won't continue the sequence from another sender. - **The account is deleted from Victoria AI.** Reconnecting it later counts as a new connection, with a new warm-up ramp. Your plan doesn't change. The seat or mailbox stays on your bill and is free for another account. To pay for fewer, remove an add-on in **Credits & Billing**. See [Plans and add-ons](https://docs.versionseven.ai/help/plans-and-add-ons). ## Who can remove a sender Owners and admins can remove any sender. A member can remove only a sender they connected themselves. If a member tries to remove someone else's sender, the app shows **Failed to remove account**; ask an owner or admin to remove it. ## Replace a sender with a different account 1. Remove the old account as above. 2. Connect the new one: [LinkedIn](https://docs.versionseven.ai/help/connect-linkedin) or [Gmail or Outlook](https://docs.versionseven.ai/help/connect-gmail-or-outlook). 3. Assign the new account to each campaign in **Campaigns → your campaign → Settings → Connected Accounts**. 4. Turn the paused campaigns back on. The preflight checks run again. See [Preflight checks](https://docs.versionseven.ai/help/preflight-checks). The leads the old sender stopped don't move to the new one on their own. If the account is only disconnected rather than wrong, reconnect it instead: that keeps its campaigns and leads. See [Reconnect a sender](https://docs.versionseven.ai/help/reconnect-a-sender). ## Replace a sender during the free trial The trial allows 1 LinkedIn account and 1 mailbox. To swap one, remove the old account first, then connect the new one in the freed slot. Keep at least one sender connected through the end of the trial. A removed sender doesn't count toward the trial's charge rule, so a workspace with no sender at the end closes. See [Free trial](https://docs.versionseven.ai/help/free-trial). ## Revoke a pending sender invite link A row marked **Pending** is an invite link, not an account. Its trash icon opens **Revoke invite link?**; select **Revoke** to free the slot. See [Connect a sender with an invite link](https://docs.versionseven.ai/help/connect-a-sender-with-an-invite-link). --- Source: https://docs.versionseven.ai/help/find-leads-in-the-lead-database # Find leads in the Lead database Search the Lead database from a campaign, preview results, and add leads with emails or LinkedIn only, with what each step costs. The Lead database is a search of business contacts you can add straight into a campaign. It opens from the campaign's **Leads** tab as **Find Leads**. **Find Leads** appears only when the Lead database is on for your workspace, which it is while you have an active trial or subscription. If you don't see the button, use [Import a CSV](https://docs.versionseven.ai/help/import-a-csv) or **Add Lead** instead. ## Search the Lead database 1. Open **Campaigns → your campaign → Leads** and select **Find Leads**. 2. Set at least one filter: **Job titles**, **Seniority**, **Locations**, **Industries**, **Company names**, **Company domains** or **Company size**. Type a title or location and press Enter to add it. 3. Choose how to add leads under **Add leads with** (see the next section). 4. Select **Search**. A page holds 25 results. Results list each person's name, title, company and location. Select **Load 25 more** for the next page, or **Back** to change the filters. No results means the filters are too narrow; remove one and search again. ## Email + LinkedIn or LinkedIn only **Add leads with** has two modes: - **Email + LinkedIn**: Victoria AI looks up a verified work email for each person you add. Choose this when the campaign has email steps. - **LinkedIn only**: leads are added with their LinkedIn profile and no email lookup. Choose this for LinkedIn-only campaigns. It costs nothing beyond the search. An email is kept only when the lookup rates it deliverable, high-probability or catch-all, so a lead added in email mode either has an email that passed that check or has none. ## Add selected leads from the Lead database Tick the people you want (or **Select all**), then select **Add**. You can select up to 100 at a time. The footer shows how many credits will be reserved before you confirm. In email mode, the emails are found in the background. The **Leads** tab shows the progress, and the leads appear as their lookups finish. ## Add many matching leads at once To add more than one page, use **Add up to … matching leads** above the results: 1. Enter a number, up to 500 per batch. 2. Select **Review cost**. It shows the search cost, the most the email lookups could cost, and your balance. 3. Select **Confirm**. The full amount is reserved, and the unused part is refunded as batches finish. Leads already in the campaign are skipped, and people already in your workspace are reused at no cost. Anyone on your [Do Not Contact](https://docs.versionseven.ai/help/do-not-contact) list is skipped and counted in the result. ## What the Lead database costs | Action | Cost | | - | - | | Each search result returned | 0.825 credits | | Each work email found | 3.3 credits | | A LinkedIn-only add | Nothing beyond the search | A short page is refunded down to what it returned. An email lookup is reserved up front and refunded when no email is found. The **Find Leads** header shows your balance, and **Search** is disabled when you can't afford a page. See [Credits and rollover](https://docs.versionseven.ai/help/credits-and-rollover). During the trial, Lead database use is capped at 330 credits in total, and every lead counts toward the [trial lead cap](https://docs.versionseven.ai/help/trial-lead-cap). ## Lead database errors - **"Not enough credits"**: buy credits or turn on auto-recharge. See [Buy credits and auto-recharge](https://docs.versionseven.ai/help/buy-credits-and-auto-recharge). - **"Lead database temporarily unavailable"**: nothing was charged. Try again in a few minutes. - **"Lead database not enabled"**: the Lead database isn't on for this workspace. Check that your trial or subscription is active in **Credits & Billing**. - **The trial cap message** ("Your trial includes … credits for finding leads"): add fewer leads at a time, or wait until the trial converts. --- Source: https://docs.versionseven.ai/help/import-a-csv # Import leads from a CSV Upload a CSV or XLSX of leads into a campaign, map its columns, and turn extra columns into variables for your sequence. Upload a list of your own leads into a campaign from a CSV or XLSX file. You map the columns once, and any extra columns can become variables in your messages. ## Columns a lead CSV needs Every lead needs a **First Name**, a **Last Name**, and an **Email** or a **LinkedIn URL**. A lead with only an email can receive only email steps, and one with only a LinkedIn URL only LinkedIn steps. Optional standard columns: **Phone Number**, **Company**, **Job Title**, **Company Website** and **Industry**. A company website helps AI Personalization fields find facts about the company. ## Upload a CSV into a campaign 1. Open **Campaigns → your campaign → Leads** and select **Upload CSV**. 2. **Upload**: drop the file on the window or click to choose it. CSV and XLSX files work. A preview of the first rows appears. 3. **Map Fields**: under **Column Mapping**, check that each standard field points at the right column. Victoria AI guesses from the headers; pick **Skip** for any you don't want. 4. Add any **Custom Fields** (see below). 5. **Verify**: review how many leads are ready and which rows will be skipped, then select **Upload Leads**. The leads appear in the campaign shortly. A banner on the **Leads** tab shows the upload's progress and, when it finishes, how many were added and how many were on your Do Not Contact list. ## Turn extra CSV columns into custom fields and variables Any column that isn't a standard field can be kept as a custom field: 1. In **Map Fields**, under **Custom Fields**, select **Add Field**. 2. Type a name, for example `recent_funding`. Names are lowercase with underscores. 3. Pick the column it comes from. Use it in any message as `{recent_funding}`, with single braces. Before activation, the preflight checks warn if some leads have no value for a variable the sequence uses. See [Personalization fields](https://docs.versionseven.ai/help/personalization-fields) and [Preflight checks](https://docs.versionseven.ai/help/preflight-checks). ## CSV rows skipped at the Verify step The **Verify** step lists skipped rows grouped by reason, such as **Missing First Name**, with their row numbers. Select **Download … skipped rows** to get them as a CSV, fix them, and upload that file. If every row is skipped, the window says all rows are missing required fields. Check that **First Name**, **Last Name** and **Email** or **LinkedIn URL** are mapped to the right columns. ## CSV upload fails or times out - **"Upload timed out"**: split the file into smaller files and upload them one at a time. - **"No data rows found"**: the file has only a header row, or is empty. - **The trial lead cap message**: during the trial, an upload that would take the workspace over 250 leads is refused whole. Upload a smaller file. See [Trial lead cap](https://docs.versionseven.ai/help/trial-lead-cap). ## Add a single lead by hand To add one person, select **Add Lead** on the **Leads** tab. Fill in **First Name**, **Last Name**, and an **Email** or **LinkedIn URL**, plus any of **Company**, **Job Title** and **Company Website**. --- Source: https://docs.versionseven.ai/help/do-not-contact # Do Not Contact list Block emails, domains and LinkedIn profiles from every campaign, what the list blocks, and how opt-outs are added automatically. The Do Not Contact list holds the emails, domains and LinkedIn profiles that no campaign in your workspace may message. Use it for customers, competitors, partners and anyone who asked not to be contacted. ## What the Do Not Contact list blocks The list is checked: - **At import.** CSV uploads, Find Leads and **Add Lead** skip anyone on the list. CSV and Find Leads results say how many were skipped. - **Before every send.** A lead already in a campaign is skipped at its next send after you list them. - **By the Appointment Setter** before it replies. A **domain** blocks every address at that domain and its subdomains. An **email** blocks that one address. A **LinkedIn** entry blocks that profile URL. ## Add entries to the Do Not Contact list 1. Go to **Settings** and scroll to **Do Not Contact**. 2. Type an email (`name@company.com`), a domain (`company.com`) or a LinkedIn URL (`linkedin.com/in/…`). The badge next to the box shows what was detected, or **Not recognised**. 3. Add an optional **Reason**, then select **Add**. ## Paste a list into Do Not Contact 1. In **Settings → Do Not Contact**, select **Paste many**. 2. Paste one entry per line, or separate them with commas. Emails, domains and LinkedIn URLs are detected automatically. 3. Select **Add**. Up to 500 entries at a time. The result says how many were added, how many were already listed and how many weren't recognised. ## Add one lead to Do Not Contact from a campaign Open the lead from the campaign's **Leads** tab and select **Do not contact** under **Actions**, then confirm. Their email and LinkedIn URL are listed, they're removed from this campaign, and every other campaign skips them at its next send. ## Opt-outs are added to Do Not Contact automatically When a prospect asks the Appointment Setter not to be contacted again, Victoria AI adds them to the list. Their entry shows **Asked not to be contacted**. Each entry shows where it came from: **Added manually**, **Pasted list**, **From a lead**, **API** or **Asked not to be contacted**. ## Remove an entry from Do Not Contact Only owners and admins can remove entries. In **Settings → Do Not Contact**, find the entry (use **Search** for long lists) and select its trash icon. Anyone on the team can add to the list. Removing an entry doesn't put the person back into a campaign. Add them again if you want to contact them. --- Source: https://docs.versionseven.ai/help/trial-lead-cap # Trial lead cap How many leads a trial workspace can hold, what counts toward the cap, and what to do when an import is refused. During the free trial, a workspace can hold up to 250 leads. The cap lifts when the trial converts to a paid plan. ## What counts toward the trial lead cap Every lead in the workspace counts once, whichever campaign it's in and however it arrived: CSV uploads, Find Leads, **Add Lead**, the Copilot, a connected assistant and the REST API. A lead in two campaigns counts once. ## Trial lead cap reached: what you see A CSV upload or API request that would take the workspace over the cap is refused whole, not cut short. The message reads: "Your free trial allows up to … leads and … are already in your workspace (… remaining). Subscribe in Billing to add more." Over the REST API, the same refusal is `403 TRIAL_LEAD_CAP_REACHED`, with the `cap`, `used` and `remaining` counts. ## Fix a refused import on the trial - **Add fewer leads.** Upload a smaller file, or add up to the remaining number shown in the message. - **Delete leads you won't use.** An owner or admin can open a lead and select **Delete lead**, which frees its place. - **Wait for the trial to convert.** The cap lifts when billing starts: at the end of the trial, or earlier if the workspace reaches 200 sends or 20 Appointment Setter conversations. See [Free trial](https://docs.versionseven.ai/help/free-trial). ## Trial Lead database limit Separately from the lead cap, Lead database use during the trial is capped at 330 credits, covering searches and found emails together. A request that would go over it is refused with the credits it needs and the credits left. See [Find leads in the Lead database](https://docs.versionseven.ai/help/find-leads-in-the-lead-database). --- Source: https://docs.versionseven.ai/help/build-a-campaign # Build a campaign Create a campaign in Victoria AI from start to activation, in the app or with the AI Sales Copilot. A campaign is a sequence of steps, a list of leads, and the sender accounts that send for it. You can build one by hand in **Campaigns**, or ask the AI Sales Copilot to build it with you. Either way the campaign starts as a draft and sends nothing until you activate it. ## Build a campaign by hand: the steps in order 1. **Create it.** Go to **Campaigns → New Campaign**. Enter a **Campaign name**, an optional **Description**, and pick a **Template**: **Email + LinkedIn**, **LinkedIn Only**, **Email Only** or **Start from Scratch**. Select **Create Campaign**. The campaign opens on its **Sequence** tab, paused. 2. **Write the sequence.** Open **Campaigns → your campaign → Sequence**. Click each step to open its editor and write the message. Email steps need a subject and a body. See [Sequence steps and branches](https://docs.versionseven.ai/help/sequence-steps-and-branches). 3. **Add personalization (optional).** In any message, use **Insert Variable** to add lead fields such as `{first_name}`, or add an AI Personalization field. See [Personalization fields](https://docs.versionseven.ai/help/personalization-fields). 4. **Add leads.** Open **Leads** and choose **Find Leads**, **Add Lead** or **Upload CSV**. See [Find leads in the Lead database](https://docs.versionseven.ai/help/find-leads-in-the-lead-database) and [Import a CSV](https://docs.versionseven.ai/help/import-a-csv). 5. **Assign senders.** Open **Settings → Connected Accounts** and select each account that should send. Connect accounts first on **Sender Accounts** if none are listed. See [Connect LinkedIn](https://docs.versionseven.ai/help/connect-linkedin) and [Connect Gmail or Outlook](https://docs.versionseven.ai/help/connect-gmail-or-outlook). 6. **Check work hours and daily limits.** Still on **Settings**, review **Work Hours** and **Daily Limits**. See [Work hours and daily limits](https://docs.versionseven.ai/help/work-hours-and-daily-limits). 7. **Set up replies (optional).** Open **Appointment Setter** to let the AI agent answer replies. See [Appointment Setter](https://docs.versionseven.ai/help/appointment-setter). 8. **Activate.** Turn on the switch next to **Paused** at the top of the campaign. The readiness check runs; when every check passes, select **Activate Campaign**. See [Preflight checks](https://docs.versionseven.ai/help/preflight-checks). ## Build a campaign with the AI Sales Copilot Select **AI Sales Copilot** in the sidebar and describe who you sell to and what you offer. The Copilot can create the campaign, write every step of the sequence, create AI Personalization fields, find leads in the Lead database, assign senders and run the readiness check. Every change it proposes appears on an approval card first, and nothing happens until you approve it. Actions that spend credits, like finding leads, show the cost on the card. The Copilot always creates campaigns as drafts. It activates one only when you approve the activation card, and it uses the same readiness check as the app. If you connect Claude or ChatGPT to your workspace, that assistant can do the same work from outside the app. See [Connect your AI assistant](https://docs.versionseven.ai/help/connect-your-ai-assistant). ## Pick a campaign template The template only decides the starting steps; you can change everything afterwards. - **Email + LinkedIn**: views the profile, sends a connection request, then branches. People who accept get LinkedIn messages and emails; people who don't get emails. - **LinkedIn Only**: the same start, then LinkedIn messages for people who accept and nothing for people who don't. - **Email Only**: three emails. - **Start from Scratch**: an empty sequence. Choose a template that matches the data your leads have. Leads without an email address can't receive email steps, and leads without a LinkedIn URL can't receive LinkedIn steps. The [starter sequences](https://docs.versionseven.ai/playbook/sequences/multichannel-b2b-default) in the Playbook show each shape with full example copy. ## What happens after you activate a campaign Victoria assigns each lead to one of the campaign's senders and starts the sequence. Steps go out only inside the campaign's work hours and within its daily limits and each sender's own limits, so the first sends can take a while to appear. When a lead replies, the sequence stops for that lead and the reply shows in the **Inbox**. If nothing seems to happen, see [Leads aren't moving](https://docs.versionseven.ai/help/troubleshooting-leads-not-moving). --- Source: https://docs.versionseven.ai/help/sequence-steps-and-branches # Sequence steps and branches Every step type in a Victoria AI sequence, how delays work, and how the connection-accepted branch splits leads. A sequence is the list of steps each lead goes through. You edit it in **Campaigns → your campaign → Sequence**. Changes save automatically; the editor shows **Saving…** and then **Saved**. ## Sequence step types Add a step with the **+** button below the last step, or inside a branch. The menu offers only the steps allowed at that point. | Step | What it does | Content | | - | - | - | | **View Profile** | Visits the lead's LinkedIn profile. | None | | **Connection Request** | Sends a LinkedIn connection request. | An optional note of up to 300 characters | | **If/Then Branch** | Checks whether the lead accepted the connection request and splits into **Yes** and **No** paths. | None; added automatically | | **LinkedIn Message** | Sends a LinkedIn direct message. | Message text | | **Email** | Sends an email. | Subject and body | The connection note limit counts the message after variables are filled in, so a long company name can push a note over. The editor shows a character counter. A sequence can hold up to 40 steps, counting both A/B variations and every branch. ## Step delays in a sequence Every step except the branch has a **Wait after this step** setting, in whole days. It's the gap between this step and the next one for the same lead. A wait of 0 sends the next step on the same day. The editor labels each step with the day it falls on. The wait starts when the step actually goes out, not when the lead was added. A step only goes out inside the campaign's work hours and daily limits, so real gaps can be longer than the setting. The longest wait on one step is 90 days. ## The connection-accepted branch When you add a **Connection Request**, the editor adds an **If/Then Branch** right after it. The two are a pair: deleting either one deletes the other and every step inside the branches. How the branch decides: 1. The connection request goes out. 2. Victoria waits for the request's **Wait after this step**. That wait is the lead's window to accept. 3. Victoria checks the lead's LinkedIn connection once. If they're connected, the lead takes **Yes**; if not, **No**. The check happens once. A lead who accepts after the check stays on the **No** path. If the **No** path is empty and the lead hasn't accepted, Victoria checks again up to 3 times, 24 hours apart, before marking the lead complete. The branch has its own wait between the check and the first step on either path. The built-in templates set one, and the **Sequence** tab doesn't show or change it, so the first branch step can go out later than its day label suggests. ## Which steps are allowed where in a sequence - **Before a connection request**: View Profile, Email, Connection Request. - **Yes path** (accepted): View Profile, LinkedIn Message, Email. - **No path** (not accepted): View Profile, Email. LinkedIn messages only go to people you're connected with, so a **LinkedIn Message** step exists only on the **Yes** path. Once a sequence has a connection request, new steps go inside its branches. ## Multichannel or single-channel sequences A **multichannel** sequence uses both LinkedIn and email. The usual shape is a connection request first, LinkedIn messages and emails on **Yes**, and emails on **No**. It reaches the most leads, and it needs a LinkedIn account and a mailbox assigned to the campaign. A **LinkedIn-only** sequence needs only a LinkedIn account and reaches only leads with a LinkedIn URL. With an empty **No** path, people who don't accept get nothing further. An **email-only** sequence needs only a mailbox and reaches only leads with an email address. A lead who lacks the data for a step skips it: an email step is skipped for a lead with no email address. The readiness check warns, or blocks, when many leads can't be reached by the sequence. See [Preflight checks](https://docs.versionseven.ai/help/preflight-checks). ## Preview a sequence step before it sends Open an email, connection request or LinkedIn message step and select **Preview**. Pick one of the campaign's leads to see the message with its fields filled in. If the message uses AI Personalization fields, **Generate** writes them for that lead, using the same research as a real send. See [Personalization fields](https://docs.versionseven.ai/help/personalization-fields). --- Source: https://docs.versionseven.ai/help/personalization-fields # Personalization fields and variables Lead variables, custom fields and AI Personalization fields in Victoria AI messages, with fallbacks, coverage, held leads and costs. Messages can include variables in single curly braces, such as `{first_name}`. Victoria fills them in for each lead when the step sends. There are three kinds: lead fields, custom fields from your CSV, and AI Personalization fields that Victoria writes for each lead. ## Insert a variable into a message Open **Campaigns → your campaign → Sequence**, click a step, and select **Insert Variable** under the field you're editing. The menu has three sections: - **Standard**: First Name, Last Name, Company, Job Title, Email, LinkedIn URL. - **Custom Fields**: the extra columns from leads you uploaded by CSV. - **AI Personalization**: the campaign's AI fields, with **+** to add one. Always use single braces. `{{first_name}}` doesn't work, and the readiness check blocks a sequence that uses it. > **Warning:** Use `{first_name}`, `{last_name}`, `{company}` and `{title}` in message copy. The menu also lists Email and LinkedIn URL, but the sender doesn't fill `{email}` or `{linkedin_url}` into a message, so a step that uses them fails to send. ## What a blank lead field becomes in a message When a lead has no value for a standard field, the send still goes out with a stand-in: - `{first_name}` becomes "there", so "Hi `{first_name}`," reads "Hi there,". - `{company}` becomes "your company". - `{last_name}` and `{title}` become empty. Write copy that still reads well with those stand-ins. A custom field the lead has no value for is reported by the readiness check instead; see "Lead data coverage for variables" below. ## Add an AI Personalization field An AI Personalization field is a variable Victoria researches and writes for each lead before the step sends. 1. In the step editor, open **Insert Variable** and select **+** next to **AI Personalization**. 2. Enter a **Field Name**, such as `recent_achievement`. Use lowercase letters and underscores. 3. Write **AI Instructions**: what to write, how long, and from what. "In one sentence, name something specific the company launched or announced recently" works better than "Find information about this company". 4. Optionally enter a **Fallback value**, up to 400 characters. 5. Choose **Data Sources**: **Website**, **LinkedIn Profile** or both. 6. Select **Add Field**, then insert it with **Insert Variable → AI Personalization**. AI fields can go in email subjects, email bodies and LinkedIn messages. Connection notes offer only standard and custom fields. The [personalization instructions playbook](https://docs.versionseven.ai/playbook/personalization-instructions) has instruction patterns that work. ## How an AI Personalization field is filled When a step that uses an AI field is about to send, Victoria reads the lead's LinkedIn profile and recent posts, the company website, or both, and writes the value. It writes only what those sources support. If the sources don't support the instructions, it works down this ladder: 1. **Generated**: the research supports a value. It's used. 2. **Fallback**: a fallback value is set. It's used. 3. **Generic**: no fallback is set. Victoria writes a neutral line from the lead's basic fields: name, title, company, industry and size. 4. **Held**: nothing usable came out. The lead is held for 24 hours and tried again. It's not skipped and nothing is sent in the meantime. Set a fallback on every field so you control what a lead gets when research finds nothing. Without one, a lead can get the generic line or wait. ## Preview AI Personalization results for a lead Open the step and select **Preview**, pick a lead, and select **Generate**. The preview runs the same research as a real send and marks each value: - highlighted: generated from research; - **Fallback**: the fallback value was used; - **Generic**: a neutral line was written because there's no fallback; - underlined amber: the lead would be **held for 24 hours and retried**, with what was missing; - underlined red: generation failed; select **Try again**. **Add a fallback value →** opens the field so you can fix it from the preview. Generating a preview spends credits, the same as a send. ## Lead data coverage for variables Before activation, the readiness check counts how many enrolled leads lack data for each lead field or custom field the sequence uses: - If more than 10% of leads are missing a variable, activation is blocked until you add the data or remove the variable. - At or below that, it's a warning. You can activate, and those leads get the blank-field stand-ins above. AI fields aren't part of this check; they're written at send time. See [Preflight checks](https://docs.versionseven.ai/help/preflight-checks). ## What AI Personalization costs Writing a lead's AI fields costs about 3 credits per lead on average. The cost is metered from how much the model reads and writes, so it varies. Credits are spent when the step sends, or when you select **Generate** in a preview. Creating or editing a field costs nothing. Lead fields and custom fields cost nothing. When the balance reaches zero, sending stops. See [Credits and rollover](https://docs.versionseven.ai/help/credits-and-rollover). ## Use fewer AI Personalization fields One or two AI fields per sequence, in the first message after connecting or the first email, is the pattern the Copilot follows. Each field adds cost and another chance of a held lead. Never put two in one message. See [Personalization that works](https://docs.versionseven.ai/playbook/personalization-instructions). --- Source: https://docs.versionseven.ai/help/ab-testing # A/B test a sequence Run two variations of a Victoria AI sequence, split new leads between them, read the verdict, and end the test. An A/B test runs two versions of a campaign's sequence, Variation A and Variation B, on the same leads list. Each new lead is assigned to one variation and stays on it. ## Turn on A/B testing for a campaign 1. Open **Campaigns → your campaign → Sequence**. 2. Select **Enable A/B Testing**. Variation B starts as a copy of A and the editor switches to B. 3. Change B. Use the **A** and **B** buttons to switch between them. Make the variations differ in angle, not just wording: a different opening, offer or call to action. Two near-identical versions rarely show a difference. If B is empty, **Copy from Variation A** fills it. The readiness check adds two checks while A/B is on: **Variation B has steps** and **Variation B steps have content**. ## Set the A/B traffic split **Split** next to the variation buttons sets the share of new leads that get A: **A 50 / B 50** through **A 90 / B 10**. It applies to leads added from then on. Leads already in the campaign keep their variation. Leads that were in the campaign before you turned A/B on are **Unassigned**. They run Variation A, count in **All**, and count in neither arm. ## Read the A/B verdict Open **Campaigns → your campaign → Analytics**. With A/B on, the tab shows an **A/B verdict** card and a funnel for each variation, and a filter at the top (**All**, **A**, **B**, **Unassigned**) narrows every number on the page to one arm. The verdict compares each arm's positive reply rate (leads with a positive reply ÷ leads contacted) with a two-proportion z-test: | Verdict | Meaning | | - | - | | **Not enough data yet** | An arm has fewer than 50 contacted leads. | | **No meaningful difference** | The arms are too close to call. | | **Leaning A** or **Leaning B** | One arm is ahead, with p below 0.2. Keep it running. | | **A wins** or **B wins** | One arm is ahead, with p below 0.05. | The arm a lead belongs to is the variation it was assigned when enrolled. A + B + Unassigned always equals All. ## Promote the winning variation The Sequence tab has no promote button. Ask the AI Sales Copilot, for example "promote variation B on this campaign", and approve the card. A connected assistant can do the same with the `promote_ab_winner` tool. - Promoting **A** sends every new lead to A. Leads already on B finish B. - Promoting **B** copies B over A and sends every new lead to it. Leads still in progress on A continue on B's copy from their current step number. A/B testing stays on after a promotion, so leads mid-way through B finish its copy. ## Turn off A/B testing Select **Disable A/B**, then **Delete Variation B**. Turning A/B off deletes Variation B's steps. Leads that were on B continue on Variation A from the same step number, which can skip or repeat a step if the two variations have different lengths. To keep B's copy, promote B first instead. --- Source: https://docs.versionseven.ai/help/work-hours-and-daily-limits # Work hours and daily limits Set when a Victoria AI campaign sends and how much it sends each day, and how campaign limits combine with sender limits. Each campaign has its own sending schedule and daily caps, in **Campaigns → your campaign → Settings**. Every sender account also has its own limits, and the lowest limit that applies always wins. ## Set a campaign's work hours 1. Open **Campaigns → your campaign → Settings → Work Hours**. 2. Pick a **Timezone**. 3. Turn each day on or off and set its start and end times. 4. Select **Save**. A new campaign starts with 9 AM to 5 PM, Monday to Friday, US Eastern time. Steps only go out inside the window, in the timezone you pick. A step that falls due outside it waits for the next window, so on the default schedule a lead added on Friday evening gets its first step on Monday. > **Warning:** Turning the **Work Hours** switch off doesn't mean "send any time". A campaign without active work hours sends on the default schedule, 9 AM to 5 PM, Monday to Friday, US Eastern time. To send on weekends or in the evening, keep the switch on and turn those days on with the hours you want. The Appointment Setter follows the same work hours for its replies unless you turn off **Respect work hours** in its settings. See [Appointment Setter](https://docs.versionseven.ai/help/appointment-setter). ## Set a campaign's daily limits Open **Campaigns → your campaign → Settings → Daily Limits**. Each row caps one action per day for each sender account in this campaign: with two LinkedIn accounts assigned, each can send up to the **Connection Requests** limit for this campaign. Change the number and select **Save** on that row. | Limit | New campaign default | | - | - | | **Profile Views** | 30 | | **Connection Requests** | 20 | | **LinkedIn Messages** | 20 | | **Emails** | 30 | The LinkedIn rows can't go above 30. **Emails** can go up to 500. ## How campaign limits combine with sender limits Every sender account has its own caps on top of the campaign's: - LinkedIn connection requests and messages are capped at 20 a day per account, and connection requests at 100 a week. An owner can raise these to 30 a day and 200 a week on **Sender Accounts** after acknowledging the risk. - Every newly connected account ramps up over 14 days before it reaches its full limit. - A LinkedIn restriction halves the account's LinkedIn caps for a while. So a campaign whose limit is higher than its accounts' caps still sends only what each account allows that day. [Sender limits and ramp](https://docs.versionseven.ai/help/sender-limits-and-ramp) has the full ramp schedule, and [LinkedIn restrictions and limits](https://docs.versionseven.ai/help/linkedin-restrictions-and-limits) covers restrictions. ## Send more from a campaign each day When a campaign sends less than you want, check in this order: 1. **Sender caps**: an account still ramping, or at its daily or weekly ceiling, sets the real limit. Add sender accounts to the campaign to send more in parallel. 2. **Daily limits**: raise the campaign's row for that action. 3. **Work hours**: a short window leaves less time to send. Raising limits on a new LinkedIn account doesn't skip its ramp. --- Source: https://docs.versionseven.ai/help/preflight-checks # Preflight checks Every readiness check Victoria AI runs before a campaign activates, what each message means, and how to fix it. When you turn a campaign on, Victoria runs a readiness check before anything sends. The same check runs whether you activate in the app, through the AI Sales Copilot, or from a connected assistant or the API. ## How the preflight works Turn on the switch at the top of **Campaigns → your campaign**. A window lists every check: - **Failed** checks block activation. **Activate Campaign** stays disabled until you fix them. - **Warnings** don't block. You can still select **Activate Campaign**; read each one first. - **Passed** checks are collapsed under "N checks passed". Each failed or warning check shows the exact problem and a **Fix →** link to the tab or page where you fix it. After fixing, close the window and turn the switch on again to re-run the check. ## Preflight check: Sequence has steps **Message:** "Sequence has no steps. Add at least one step to start outreach." The sequence (Variation A) is empty. **Fix:** open **Sequence** and add at least one step, or pick a template when you create the campaign. ## Preflight check: All steps have content **Message:** "N steps are missing content." An email step has no subject or no body, or a LinkedIn message step has no text. This blocks activation. Connection requests without a note are fine. **Fix:** open **Sequence**, click each empty step and fill it in. ## Preflight check: Variation B has steps Shown only when A/B testing is on. **Message:** "Variation B has no steps. Add steps or disable A/B testing." **Fix:** on **Sequence**, switch to **B** and select **Copy from Variation A** or add steps. Or select **Disable A/B**. See [A/B testing](https://docs.versionseven.ai/help/ab-testing). ## Preflight check: Variation B steps have content Shown only when A/B testing is on. **Message:** "Variation B: N steps are missing content." This blocks activation, like the check for Variation A. **Fix:** switch to **B** on **Sequence** and fill in every email and LinkedIn message. ## Preflight check: Variables resolve **Message:** "Unknown variables: `{x}`. Check for typos or add them as custom/AI fields." A variable in the copy isn't a lead field, a custom field from your leads, or one of the campaign's active AI Personalization fields. Variable names must match exactly. **Fix:** correct the spelling, or create an AI Personalization field with that name, or upload leads with a column of that name. See [Personalization fields](https://docs.versionseven.ai/help/personalization-fields). ## Preflight check: All leads have data for sequence variables **Message (warning):** "N lead-variable gaps", naming each variable and how many leads lack it, ending "you can activate, or enrich them first." **Message (blocking):** the same list, ending "upload or enrich before activating." Some enrolled leads have no value for a lead field or custom field the sequence uses. At or below 10% of leads missing a variable, it's a warning; above that, it blocks. AI fields aren't counted. **Fix:** fill in the missing data on **Leads** (re-upload or enrich), remove those leads, or take the variable out of the copy. ## Preflight check: No double-bracket variables **Message:** "Double-bracket variables: `{{x}}`", followed by a reminder to use single brackets, such as `{first_name}`. `{{first_name}}` isn't a variable in Victoria and would send a broken message. **Fix:** on **Sequence**, change every `{{name}}` to `{name}`. ## Preflight check: Accounts assigned **Message:** "No accounts assigned. Assign at least one in Settings." **Fix:** open **Settings → Connected Accounts** and select at least one account. If the list is empty, connect one on **Sender Accounts** first. ## Preflight check: Leads, sequence, and accounts align This check fails or warns for any of these messages: - "Sequence has LinkedIn steps but no LinkedIn account is assigned." **Fix:** assign a LinkedIn account in **Settings → Connected Accounts**. - "Sequence has email steps but no email account is assigned." **Fix:** assign a mailbox in **Settings → Connected Accounts**. - "N of M leads have no email", followed by "add a LinkedIn step or remove LinkedIn-only leads" or "They will be skipped". The sequence has no LinkedIn steps, but some leads only have a LinkedIn URL. It warns when 10% or fewer of the leads are affected (they're skipped) and blocks above that. **Fix:** add LinkedIn steps, or remove those leads, or add their emails. - "N LinkedIn-only leads", followed by "assign a LinkedIn account in Settings". **Fix:** assign a LinkedIn account in **Settings → Connected Accounts**. - "N of M leads have no LinkedIn URL", followed by "add an email step or remove email-only leads" or "They will be skipped". The sequence has no email steps, but some leads only have an email. Same thresholds. **Fix:** add email steps, or remove those leads. ## Preflight check: Accounts not active in another campaign **Message:** "N accounts are also active in "Other campaign". Each account should run in one campaign at a time." An account you assigned already sends for another active campaign. This blocks activation. **Fix:** pause the other campaign, or unassign the account in this campaign's **Settings → Connected Accounts** and assign a different one. Settings marks an account in use as **Active in:** with the other campaign's name. ## Preflight check: Accounts within subscription limit This check fails for either message: - "N assigned accounts are no longer active. Reconnect on the Sender Accounts page." An assigned account is disconnected. **Fix:** reconnect it on **Sender Accounts**, or unassign it. See [Reconnect a sender](https://docs.versionseven.ai/help/reconnect-a-sender). - "You have N active LinkedIn accounts but your subscription covers M. Increase quantity in Billing." You have more active accounts on one platform than your plan covers. **Fix:** add seats or mailboxes in **Credits & Billing**, or disconnect accounts you don't use. ## Preflight check: Email sender domains authenticated (SPF, DKIM, DMARC) Victoria checks the DNS records of each assigned mailbox's domain. A result older than 24 hours is checked again during the preflight. - **Blocking:** "… failed the email authentication check (no SPF record). Fix SPF/DMARC on the Sender Accounts page before activating." **Fix:** add or correct the record named in the message, then re-check on **Sender Accounts**. See [DNS setup](https://docs.versionseven.ai/help/dns-setup). - **Warning:** "… passed the email authentication check with warnings." You can activate; the details are on **Sender Accounts**. - **Warning:** "We couldn't verify SPF, DKIM and DMARC for … yet." The check couldn't complete. You can activate; it retries on its own. ## Preflight check: Leads enrolled **Message:** "No leads enrolled. Upload leads in the Leads tab." **Fix:** open **Leads** and use **Find Leads**, **Add Lead** or **Upload CSV**. ## Preflight check: Active subscription This check fails with one of these messages. Every fix is on **Credits & Billing**: - "No subscription found. Start a trial in Billing." - "Payment failed. Update your card in Billing to resume sending." See [Failed payments](https://docs.versionseven.ai/help/failed-payments). - "Subscription is canceled. Reactivate in Billing." - "Subscription is set to cancel at period end. Reactivate in Billing." - "Trial has ended. Add a payment method in Billing." See [Free trial](https://docs.versionseven.ai/help/free-trial). - "Subscription is not active. Check Billing." ## When the preflight can't run If the window says **Couldn't check readiness**, the check itself didn't complete. Select **Retry**. If it keeps failing, close the window and try again in a minute; a campaign is never activated without a completed check. --- Source: https://docs.versionseven.ai/help/edit-a-live-campaign # Edit a live campaign Which changes are safe while a Victoria AI campaign has leads in progress, and why adding or deleting steps can shift leads. You can edit a campaign while it runs, and every edit applies right away. Some edits are safe at any time; others can make leads already in the sequence skip or repeat a step. ## Why adding or deleting steps shifts leads Each lead in a campaign remembers its place as a step number, such as "next step: 4". It doesn't remember which message that was. If you insert or delete a step before a lead's position, step 4 becomes a different message, so the lead skips one step or gets one twice. When the campaign is active or has leads, the **Sequence** tab shows "Leads are already moving through this sequence." and the delete confirmation repeats the warning. ## Safe edits to a live campaign These don't move anyone: - **Change a step's text**: subject, body, message or connection note. The next lead to reach the step gets the new text. - **Change a step's wait**: a lead already waiting keeps the date it was given when its previous step sent. Leads that reach the step later use the new wait. - **Add steps at the end** of the sequence or the end of a branch. Leads still in progress continue onto them. Leads already marked complete don't restart. - **Edit an AI Personalization field**: its instructions, fallback and sources apply to the next value written. - **Change work hours, daily limits or the Appointment Setter** settings. ## Risky edits to a live campaign Avoid these once leads are in progress, or accept that some leads will skip or repeat a step: - inserting a step in the middle of the sequence or a branch; - deleting a step before the end; - deleting a **Connection Request** or **If/Then Branch**, which also deletes every step inside the branches; - turning off A/B testing, which deletes Variation B (leads on B continue on A from the same step number). To change the structure of a running campaign safely, duplicate it, build the new structure in the copy, and put new leads there. See [Pause, duplicate or archive a campaign](https://docs.versionseven.ai/help/pause-duplicate-archive). ## Change senders on a live campaign Removing an account in **Settings → Connected Accounts** on an active campaign asks you to confirm. Leads already assigned to that account stop moving until you add it back; they aren't handed to another sender. Adding an account is safe; it picks up leads that haven't been assigned yet. ## When the sequence changed somewhere else If the AI Sales Copilot or another browser tab changes the sequence while you're editing, the **Sequence** tab says so. Your edits are kept and replace the other change, or you can select **Use the other version instead**. If you see **Save failed** or "Sequence not saved", make any edit to try again. --- Source: https://docs.versionseven.ai/help/pause-duplicate-archive # Pause, duplicate or archive a campaign Stop a Victoria AI campaign for now, copy it as a starting point, or archive it for good, and what each one keeps. ## Pause a campaign Open **Campaigns → your campaign** and turn off the switch at the top. The status changes to **Paused**. While a campaign is paused: - no sequence steps go out, including to leads mid-sequence; - the Appointment Setter sends no follow-up nudges on its conversations; - leads keep their place and pick up where they were when you resume. Pausing never runs the readiness check. A step that was already being sent when you paused is checked again just before it goes out, so it won't send. ## Resume a paused campaign Turn the switch back on. The readiness check runs again, and the campaign resumes once it passes. See [Preflight checks](https://docs.versionseven.ai/help/preflight-checks). ## Duplicate a campaign Open **Campaigns → your campaign**, select the **⋯** (**Campaign actions**) menu, then **Duplicate**. The copy opens straight away, named "_original name_ (Copy)", paused. The copy keeps: - the sequence, including Variation B and the traffic split; - AI Personalization fields; - work hours and daily limits; - Appointment Setter settings and the response webhook. The copy doesn't include leads or sender accounts. Add leads and assign senders before you activate it. A sender can only send for one active campaign at a time, so pause the original if the copy uses the same accounts. ## Archive a campaign Open **Campaigns → your campaign → ⋯ → Archive** and confirm. Archiving: - stops all outreach and marks every lead in the campaign complete; - removes the campaign from the **Campaigns** list; - keeps its historical data. Archiving can't be undone in the app. If you might want the campaign back, pause it instead. ## Pause or archive: which to use - **Pause** to stop for a while and resume later with leads in place. - **Duplicate** to reuse a working setup for a new audience, or to restructure a running sequence without shifting its leads. - **Archive** when the campaign is finished and you won't send from it again. --- Source: https://docs.versionseven.ai/help/inbox-basics # Inbox basics Read and answer replies in the Victoria AI Inbox, filter conversations, close and reopen them, and see the whole team's threads. The **Inbox** collects every reply to your campaigns, from LinkedIn and email, in one list. Open it from **Inbox** in the sidebar. ## Find a conversation in the Inbox The list on the left has, from top to bottom: - **Search…**: matches a prospect's name, email or company. - **All**, **LinkedIn**, **Email**: limits the list to one channel. - **All campaigns**: pick one campaign to see only its conversations. Unread conversations sit under **New**, the rest under **Earlier**. Opening a conversation marks it read; the **Mark as read** button does the same without opening it. Each row can carry an Appointment Setter badge: **Agent handling**, **Needs you** or **You're handling**. See [Appointment Setter](https://docs.versionseven.ai/help/appointment-setter). ## Your conversations or the whole team's in the Inbox By default the Inbox shows only conversations on campaigns you created or on sender accounts you connected. To see a teammate's campaign, pick it in **All campaigns**. Links from the **Dashboard** attention panel, such as "positive replies waiting for you" or "conversations handed to you", open the Inbox across the whole team. The list then shows a **Whole team** chip, plus **Positive replies**, **Needs you** or **Unanswered** for the filter applied. Select **Clear** to go back to your own conversations. ## Reply to a prospect from the Inbox Open the conversation, write in the box at the bottom and send. The reply goes from the same sender account that ran the outreach; **Sending via** shows which one. If the prospect is on both channels, tabs above the thread switch between them. You can attach files: up to 25 MB per file on LinkedIn, and in total per message on email. If the sender account is disconnected, the composer says so and offers to reconnect it. See [Reconnect a sender](https://docs.versionseven.ai/help/reconnect-a-sender). If the Appointment Setter is handling the conversation, sending your own reply pauses the agent on that thread. See [Appointment Setter](https://docs.versionseven.ai/help/appointment-setter). ## Close or reopen a conversation in the Inbox Select the **×** (**Close conversation**) at the top of the thread. The conversation leaves the open list and the Appointment Setter stops acting on it. A toast offers **Undo**. To find it again, select **Show closed conversations** at the bottom of the list, open it, and select **Reopen**. Closing doesn't remove the lead from the campaign or add them to Do Not Contact. To stop all contact, see [Replies and sentiment](https://docs.versionseven.ai/help/replies-and-sentiment). ## Mark a meeting as booked from the Inbox When a prospect tells you they booked, select **Mark meeting booked** at the top of the conversation. It counts in **Meetings booked** in analytics, pauses the Appointment Setter on the thread, and moves the lead's CRM deal to the booked stage when there is one. **Un-mark** reverses the count and leaves the deal stage as it is. A meeting is never checked against your calendar unless a booking arrived through a connected Calendly account. See [Analytics metrics](https://docs.versionseven.ai/help/analytics-metrics). --- Source: https://docs.versionseven.ai/help/appointment-setter # Appointment Setter Set up the AI Appointment Setter on a Victoria AI campaign, choose its mode and goal, and take over or hand back conversations. The Appointment Setter is an AI agent that handles replies on a campaign. It answers questions, works each prospect toward your goal, follows up when they go quiet, and hands the conversation to you when a person is needed. It's set per campaign in **Campaigns → your campaign → Appointment Setter**. ## Turn on the Appointment Setter 1. Open **Campaigns → your campaign → Appointment Setter**. 2. Choose a **Mode**, a **Goal** and its link (below). 3. Fill in what the agent should know: **Campaign description** and **Knowledge base** at least. 4. Select **Save settings**, then turn on the switch at the top of the card. In **Autonomous** mode the switch won't turn on until there's a valid `https://` link for the goal. To try it first, select **Test it**: you play the prospect, and the agent answers with your unsaved settings and shows each decision it makes. ## Appointment Setter modes: Autonomous or Notify only - **Autonomous**: the agent replies to prospects, follows up, and escalates only when it can't make progress. - **Notify only**: the agent never messages prospects. It reads each reply, records its sentiment, and emails you a short brief so you can answer yourself. Briefs and hand-offs go to **Notification email**, which defaults to the sender account's email. ## Appointment Setter goal and booking link **Goal** is **Book a meeting** or **Drive traffic to a link**. - **Book a meeting**: enter your **Booking link**, such as a Calendly or Cal.com page. The agent shares it when the prospect shows interest and asks them to reply once they've picked a time. It never proposes times, holds slots or sends calendar invites; the link is the only way it books. - **Drive traffic to a link**: enter a **Destination link**. The agent shares it with one line of context when the prospect is interested. The agent shares the link at most 2 times in a conversation, and counts the goal as reached only when the prospect says they booked or checked it out. With a Calendly link, a panel below the link lets you connect Calendly so real bookings are matched to the campaign. ## What the Appointment Setter knows The agent answers only from what you give it. It doesn't guess pricing, contract terms, security claims, integrations or dates. - **Company name**, **Agent name** (how it signs off; defaults to the sender's name) and **Tone**: **Friendly**, **Direct** or **Formal**. Replies always go from the campaign's sender account, written in the first person. - **Campaign description**: who you're targeting and what you offer, so it can answer "what is this about?". - **Knowledge base**: facts it may state when asked, one per line. - **Custom instructions**: rules for how it works, such as "always suggest a 15-minute call". - **Always hand off when…**: topics that should always go to a person. - **Assets**: links it may share when relevant, each with a description of what it is. It shares at most one per message and never the same one twice. The [Appointment Setter configuration playbook](https://docs.versionseven.ai/playbook/appointment-setter-configuration) has guidance on writing these. ## When the Appointment Setter hands a conversation to you The agent escalates and stops replying when the prospect: - asks to speak to a person; - raises pricing, legal or contract terms, security questionnaires or a complaint; - asks something the material you gave it doesn't answer; - says they're the wrong person and names someone else; - mentions anything in **Always hand off when…**; - asks for a calendar invite. It also hands off after **Max agent replies per conversation** (default 12). A prospect on your Do Not Contact list who writes in is handed to you without a reply. A handed-off conversation shows **Needs you** in the **Inbox**, with a banner giving the reason, and you get an email at the notification address. ## How the Appointment Setter ends a conversation - **Booked or visited**: closes as won when the prospect confirms. It counts in **Meetings booked**. - **A clear no**: closes as declined, with at most a one-line acknowledgement. - **"Remove me" or "stop messaging me"**: closes as an opt-out and adds the prospect to your organization's Do Not Contact list, for every campaign and channel. - **Out of office**: it doesn't reply to an automatic reply and waits at least 7 days before the next touch. - **Silence**: it follows up on the schedule below, then stops. ## Appointment Setter follow-ups and timing - **Follow-ups when a prospect goes quiet**: how many nudges (default 5, up to 10) and the spacing in days (default 2, 3, 5, 7, 10). Each nudge takes a different angle, the last is a polite break-up, and any reply resets the clock. Follow-ups run only in **Autonomous** mode. - **Timing & limits → Reply delay (minutes)**: a short pause before answering (default 10 minutes, up to 240 minutes). - **Timing & limits → Respect work hours**: on, replies wait for the campaign's work hours; off, they go out any time. - **Timing & limits → Max agent replies per conversation**: from 1 to 40. ## Take over or hand back an Appointment Setter conversation In the **Inbox**, open the conversation: - **Take over** pauses the agent on this thread. It won't reply or follow up until you hand it back; new replies are still classified. Sending your own reply from the composer also takes over. - **Hand back to agent** lets the agent pick the thread up again. If the prospect stays quiet, it resumes following up. Closing a conversation in the Inbox or marking a meeting booked also stops the agent on that thread. See [Inbox basics](https://docs.versionseven.ai/help/inbox-basics). ## Pause the Appointment Setter To stop it for one conversation, use **Take over**. To stop it for the whole campaign, turn off the switch on the **Appointment Setter** card. Pausing the campaign also stops its follow-up nudges. ## What the Appointment Setter costs An Appointment Setter conversation costs about 1.5 credits on average, metered from the model's usage. Credits are spent as replies and nudges are written. When the balance is empty, it sends no nudges. See [Credits and rollover](https://docs.versionseven.ai/help/credits-and-rollover). During the free trial, 20 Appointment Setter conversations end the trial early and start billing. See [Free trial](https://docs.versionseven.ai/help/free-trial). --- Source: https://docs.versionseven.ai/help/replies-and-sentiment # Replies and sentiment How Victoria AI labels each reply, what happens to a lead who replies or opts out, and how to correct the record. ## What happens to the sequence when a lead replies When a lead replies on any channel, the sequence stops for that lead: no more scheduled steps go out to them in that campaign. A reply that arrives while a step is about to send holds that step, so a pitch never goes out on top of an answer. The reply lands in the **Inbox**. If the campaign runs the Appointment Setter, the agent picks it up; otherwise it waits for you. ## Reply sentiment labels Every reply is classified with one label: | Label | Meaning | | - | - | | **Positive** | Interested. | | **Negative** | Not interested. | | **Neutral** | Neither clearly interested nor uninterested, such as a question. | | **Out of office** | An automatic away reply. | The labels appear in a lead's activity on **Campaigns → your campaign → Leads** (open a lead), and they drive the **Positive replies**, **Negative replies**, **Neutral replies** and **Out of office** metrics in analytics. The Inbox's **Positive replies** filter, reached from the Dashboard, uses the same label. See [Analytics metrics](https://docs.versionseven.ai/help/analytics-metrics). ## Opt-outs and Do Not Contact When a prospect explicitly asks not to be contacted again, such as "remove me" or "stop messaging me", the Appointment Setter closes the conversation as an opt-out and adds them to your organization's Do Not Contact list. That blocks every campaign and channel from messaging them, now and in future imports. A plain "no thanks" is a decline, not an opt-out, and doesn't add them. To add someone yourself, open **Campaigns → your campaign → Leads**, open the lead, and select **Do not contact**. Anyone on the team can add to the list; only owners and admins can remove an entry, in **Settings**. See [Do Not Contact](https://docs.versionseven.ai/help/do-not-contact). ## Correct a reply's sentiment or outcome The app has no control to change a reply's sentiment label. What you can correct: - **Meeting booked**: in the **Inbox**, select **Mark meeting booked** or **Un-mark** on the conversation. This changes the **Meetings booked** count. - **Conversation state**: **Take over** or **Hand back to agent** changes who handles the thread; **Close conversation** and **Reopen** move it in and out of the open list. - **Contact**: **Do not contact** stops all future outreach to the person. ## Out-of-office replies An out-of-office reply is labelled and counted, but it isn't a real answer. The Appointment Setter doesn't reply to it and waits at least 7 days before the next touch. In analytics, **Out of office** can overlap the other labels, because the same lead may also reply properly later. --- Source: https://docs.versionseven.ai/help/analytics-metrics # Analytics metrics What every number on the Victoria AI Dashboard and campaign Analytics tab counts, its unit, and how date ranges and filters apply. The **Dashboard** and each campaign's **Analytics** tab use the same metric names with the same meanings. Hover the info icon next to a number for its definition. ## Leads or messages: the unit of each analytics metric Every metric counts **leads** unless its unit says **messages**. "Replied" is the number of people who replied, not the number of replies. Only **Messages sent**, **Emails sent**, **Emails opened** and **LinkedIn messages** count individual messages, including follow-ups. ## Funnel metrics | Metric | Counts | Unit | | - | - | - | | **Contacted** | Leads reached with a first outbound message: email, LinkedIn message or connection request. | leads | | **Replied** | Leads who replied at least once: positive, negative, neutral or out of office. | leads | | **Positive replies** | Leads whose reply was classified interested. | leads | | **Negative replies** | Leads whose reply was classified not interested. | leads | | **Neutral replies** | Leads whose reply was neither. | leads | | **Out of office** | Leads who sent an automatic away reply. Can overlap the other reply buckets. | leads | | **Meetings booked** | Leads whose conversation closed as won. See below. | leads | ## Rate metrics | Metric | Formula | | - | - | | **Reply rate** | Replied ÷ Contacted | | **Positive reply rate** | Positive replies ÷ Contacted | | **Meeting rate** | Meetings booked ÷ Positive replies | A rate shows a dash instead of a number when its denominator is zero. ## Channel metrics | Metric | Counts | Unit | | - | - | - | | **Emails sent** | Emails sent, including follow-ups. | messages | | **Emails opened** | Sent emails with at least one tracked open. | messages | | **Open rate** | Emails opened ÷ Emails sent. | rate | | **Connection requests** | Leads sent a LinkedIn connection request. | leads | | **Connected** | Leads whose connection request was first accepted in the period. | leads | | **Accept rate** | Leads who accepted ÷ leads sent a request, both in the period. | rate | | **LinkedIn messages** | LinkedIn messages sent, including follow-ups. | messages | | **Messages sent** | Emails, LinkedIn messages and connection requests sent, including follow-ups. | messages | Treat **Open rate** as directional: privacy-protected inboxes hide opens and some mail clients open every image automatically. **Accept rate** over a short range can read high, because acceptances in the range can come from requests sent before it. ## Lead status metrics on a campaign | Metric | Counts | | - | - | | **Enrolled** | Leads added to the campaign. | | **Active** | Leads moving through the sequence. | | **Not yet contacted** | Enrolled leads that haven't had any outbound yet. | | **Completed** | Leads the sequence ran out of steps for. | | **Paused** | Leads not moving and not complete: paused by you, stopped after a reply, or held by the system. | ## Step metrics on a campaign On a campaign's **Analytics** tab, the step chart counts per step: - **Reached this step**: leads the step was sent to. - **Replied to this step**: leads whose reply came after this step and before the next. Each reply is credited to the last message sent before it. - **Positive from this step**: leads whose positive reply is credited to this step. ## How date ranges count analytics metrics Pick a range at the top right: **7d**, **30d**, **90d**, **All** (on the Dashboard) or **Lifetime** (on a campaign), or **Custom**. A lead counts as contacted, replied or positive in the range when its **first** such event falls inside it. Meetings count by the date the meeting was confirmed. The **Campaigns** list always shows lifetime totals: **Messages sent** in messages and **Replied** in leads. ## Filter analytics by channel or A/B variation - **Channel**: the Dashboard can be narrowed to email or LinkedIn. - **Variation**: on a campaign with A/B testing, **All**, **A**, **B** and **Unassigned** narrow every number to one arm. A lead's arm is the variation it was assigned on enrolment, and A + B + Unassigned = All. See [A/B testing](https://docs.versionseven.ai/help/ab-testing). The range and filters are kept in the page address, so a copied link opens the same view. ## Meetings booked are not checked against a calendar **Meetings booked** counts a lead when the prospect told the Appointment Setter they booked, or when someone selected **Mark meeting booked** in the Inbox. For a campaign whose goal is a link visit, it counts that confirmed goal instead. Victoria doesn't check these against a calendar, so a prospect who says they booked and never does still counts. Use **Un-mark** in the Inbox to correct one. ## Drill into an analytics number Select a metric tile to plot it on the trend chart. Where a number links to its leads, selecting it opens the list of leads behind it, with the date of the event that counted them. If a number can't load, the tile shows an error with a retry instead of a zero. --- Source: https://docs.versionseven.ai/help/campaign-health # Campaign health indicators What the warning icons and banners on a Victoria AI campaign mean, and how to clear them. Victoria keeps checking each campaign after it's live, using the same rules as the activation [preflight checks](https://docs.versionseven.ai/help/preflight-checks). When something breaks, such as a sender disconnecting or a payment failing, the campaign shows it in two places. ## The health icon on the Campaigns list On **Campaigns**, an icon can appear next to a campaign's **Active** or **Paused** status: - **Red**: at least one problem that stops sending. - **Amber**: warnings only; sending continues. Hover the icon to see "N issues preventing full activity" and each message. No icon means no issues were found. ## The health banner on a live campaign On an active campaign's page, a banner appears above the tabs when there are issues: - **This campaign isn't sending**: at least one problem blocks outreach until you fix it. - **Campaign has warnings**: review them; sending continues. Each line has a **Fix →** link that opens the tab or page where you fix it, such as **Settings** for an unassigned account or **Sender Accounts** for a disconnected one. The banner disappears once the issues are resolved. ## Common campaign health issues on a live campaign Most issues on a running campaign come from something that changed after activation: - **An assigned account is no longer active**: a sender disconnected. Reconnect it on **Sender Accounts**. See [Sender disconnected](https://docs.versionseven.ai/help/troubleshooting-sender-disconnected). - **Payment failed**: sending is paused until the card is updated in **Credits & Billing**. See [Failed payments](https://docs.versionseven.ai/help/failed-payments). - **More active accounts than the subscription covers**: add seats or mailboxes in **Credits & Billing**, or disconnect an account. - **An account is also active in another campaign**: each account should send for one campaign at a time. Pause one campaign or reassign the account in **Settings → Connected Accounts**. For every message and its fix, see [Preflight checks](https://docs.versionseven.ai/help/preflight-checks). If the campaign shows no issues but leads still aren't moving, see [Leads aren't moving](https://docs.versionseven.ai/help/troubleshooting-leads-not-moving). --- Source: https://docs.versionseven.ai/help/connect-your-ai-assistant # Connect your AI assistant Connect Claude or ChatGPT to your Victoria AI workspace in three steps, and choose read-only or read-and-write access. You can connect Claude or ChatGPT to your Victoria AI workspace and ask it to check campaigns, find leads or build a campaign, using the same tools as the in-app AI Sales Copilot. There's no key and nothing to install. ## Connect Claude or ChatGPT to Victoria AI 1. **Add the connector.** In Claude: **Settings → Connectors → Add custom connector**, paste `https://api.versionseven.ai/mcp` and save. In ChatGPT: **Settings → Connectors → Create** (with developer mode on), and paste the same URL. 2. **Sign in and approve.** Sign in with your Victoria account, pick your workspace if you have more than one, choose **Read and write** or **Read only**, and select **Allow**. 3. **Ask for something.** Start a chat with the connector on, for example "Which of my campaigns got replies this week?". ## Read only or read and write access You choose at the consent screen: - **Read only**: the assistant can look at campaigns, leads, senders and analytics but can't change anything or spend credits. - **Read and write**: it can also create and edit campaigns, add leads and activate. The assistant asks you to confirm each change, and actions that spend credits say what they cost. To change the choice later, disconnect it in **Settings → Connect your AI** and connect again. ## What a connected assistant can't do It can't read or send Inbox messages, and it can't change billing. Answer replies in the **Inbox**. The full tool list, Claude Code, Cursor and API key setup are in [Connect your AI](https://docs.versionseven.ai/guides/connect-your-ai). --- Source: https://docs.versionseven.ai/help/plans-and-add-ons # Plans and add-ons What the Workspace and Agency plans include, how to add LinkedIn seats and mailboxes, and how removing an add-on works. Victoria AI has two monthly plans. Both include every feature, unlimited users and unlimited campaigns; they differ in how many senders they include and how many credits they grant each month. There is no annual plan. ## What the Workspace and Agency plans include | | Workspace | Agency | | - | - | - | | Price per month | $99 | $999 | | LinkedIn seats included | 1 | 10 | | Mailboxes included | 3 | 20 | | Credits each month | 1,500 credits | 6,000 credits | | Add-ons | Yes | No, the quotas are fixed | The free trial is on the Workspace plan. Agency isn't a self-serve checkout in the app; contact us from **Feedback & Support** to move to it. ## Workspace add-ons: extra LinkedIn seats and mailboxes | Add-on | Price per month | Extra credits each month | | - | - | - | | LinkedIn seat | $59 | 500 credits | | Mailbox | $12 | 150 credits | Add-ons are available on a paid Workspace plan. They're locked during the trial, on Agency, and while a subscription is set to cancel. ## Add a LinkedIn seat or mailbox 1. Go to **Credits & Billing**. Under **Plan**, find **LinkedIn seats** or **Mailboxes**. 2. Select **+** on that row. 3. Review the dialog: the new monthly price and the extra credits per cycle. Confirm. You're charged a prorated amount for the rest of the current billing cycle, right away. The new seat or mailbox is available to connect at once. Its extra credits arrive with your next cycle's grant. Only owners and admins can change add-ons. ## Remove a LinkedIn seat or mailbox add-on 1. Go to **Credits & Billing → Plan** and select **−** on **LinkedIn seats** or **Mailboxes**. You can't go below what the plan includes. 2. The **Remove a LinkedIn seat** or **Remove a mailbox** dialog shows the new monthly cost and, if you have more senders connected than the new total allows, which accounts will be removed and which campaigns will pause. 3. Confirm. How removals work: - **No proration.** You aren't refunded for the rest of the current cycle; the lower price applies from the next invoice. - **Newest accounts are removed first.** If the new total is below the number connected, the most recently connected accounts on that platform are disconnected and deleted, and the active campaigns that used them pause. - **No accounts are touched** if you're already under the new total. To keep a particular account, remove a different one on **Sender Accounts** before you remove the add-on. See [Remove or replace a sender](https://docs.versionseven.ai/help/remove-or-replace-a-sender). ## Plan add-on buttons are disabled The text under the plan explains why: - "Add-ons unlock when your trial converts to a paid plan." - "Agency includes fixed seats and mailboxes. Contact support to change them." - "Reactivate your subscription to change seats or mailboxes." See [Cancel or reactivate](https://docs.versionseven.ai/help/cancel-or-reactivate). - "This subscription is on the previous per-account pricing and moves to Workspace at its next renewal." --- Source: https://docs.versionseven.ai/help/credits-and-rollover # Credits and rollover What spends credits in Victoria AI, what is free, how the monthly grant rolls over, and what stops when the balance reaches zero. Credits pay for the AI work Victoria AI does for you. Your plan grants credits every month, and you can buy more at any time. Your balance is on **Credits & Billing** under **Available credits**. ## What spends credits | Work | Cost | | - | - | | Writing a lead's AI Personalization fields | About 3 credits per lead | | An Appointment Setter conversation | About 1.5 credits | | A Lead database search result | 0.825 credits | | A work email found in the Lead database | 3.3 credits | | The weekly campaign review | Up to 10 credits per active campaign per week | Personalization and Setter costs are averages; they depend on how much the AI reads and writes. Credits are spent as the work happens: a personalization field costs nothing until a lead is personalized, and the Setter costs nothing until it answers a reply. ## What doesn't spend credits - **Talking to the AI Sales Copilot.** Chat, questions and the guided setup are free. Only the Copilot's actions that do paid work, such as finding leads, spend credits, and the approval card shows the cost first. Chat has a daily usage limit per workspace; if you reach it, the Copilot says so, and it resets within 24 hours. - **Sending.** Emails, connection requests and LinkedIn messages don't cost credits themselves. - **LinkedIn-only adds** from the Lead database, beyond the search. The weekly campaign review runs only while your balance is above 50 credits. Turn it off in **Settings → Weekly campaign review**. ## Monthly credit grant and rollover Each billing cycle your plan grants its credits: 1,500 credits on Workspace, plus 500 credits per added LinkedIn seat and 150 credits per added mailbox; 6,000 credits on Agency. The trial grants 400 credits once. Unused plan credits roll over for 1 cycle. Your plan credits never go above 2 times your monthly grant: a grant that would pass that cap tops you up only to the cap. Setup credits count as plan credits. Purchased credits are separate. They never expire and don't count toward the cap. Plan credits are spent first, then purchased credits. ## What happens when credits run out At zero, Victoria AI pauses the work that runs on its own: campaigns stop sending, AI Personalization and Lead database enrichment stop, and the Appointment Setter stops replying. Your campaigns, leads and settings stay as they are, and everything resumes once credits are added. The workspace owner gets an email when the balance gets low. To avoid running out, buy credits or turn on auto-recharge. See [Buy credits and auto-recharge](https://docs.versionseven.ai/help/buy-credits-and-auto-recharge). ## What happens to credits when you cancel When a subscription ends, unused plan credits (including trial and setup credits) are forfeited. Purchased credits stay yours. See [Cancel or reactivate](https://docs.versionseven.ai/help/cancel-or-reactivate). --- Source: https://docs.versionseven.ai/help/buy-credits-and-auto-recharge # Buy credits and set up auto-recharge Buy a credit pack from Credits & Billing, set auto-recharge to top up when your balance runs low, and fix a purchase that didn't land. You can add credits at any time without changing your plan. Purchased credits never expire. ## Buy a credit pack 1. Go to **Credits & Billing** and scroll to **Buy Credits**. 2. Choose a pack: **Starter**, **Growth** or **Scale**. Each card shows its credits and price; credits cost $25 per 1,000. 3. Pay on the Stripe checkout page. 4. You return to **Credits & Billing** with the message **Credits added!** and the new balance. Anyone in the workspace can buy credits. Each purchase appears under **Purchase History**. ## Turn on auto-recharge Auto-recharge buys credits for you when your balance drops below a level you set. 1. Go to **Credits & Billing → Auto-Recharge** and turn on **Enable auto-recharge**. 2. In **Recharge when balance drops below**, enter a balance in credits. 3. In **Purchase amount ($)**, enter a whole number of dollars from $5 to $500. 4. Select **Save Settings**. The trigger balance must be lower than the credits one recharge adds, or it would recharge again at once. Only owners and admins can change auto-recharge. ## How auto-recharge charges your card - It checks balances every hour and charges the card saved on your account. - It recharges at most 3 times in 24 hours, and waits at least 1 hour between recharges. - It doesn't run while a subscription payment has failed. See [Failed payments](https://docs.versionseven.ai/help/failed-payments). - The workspace owner gets an email for each recharge, and another if the card is declined. To change the saved card, select **Manage billing in Stripe**. ## Credits not added after a purchase - **"Payment still processing"**: Stripe is still finishing the payment. Refresh the page in a few seconds and the credits appear. - **"Payment received but credits not added"**: a banner appears at the top of **Credits & Billing** with a reference code. Select **Retry**. If it still fails, send the reference from **Feedback & Support**. Retrying never adds the same purchase twice. ## Auto-recharge declined If the card is declined, no credits are added and the owner gets an "Auto-recharge failed: update your card" email. Select **Manage billing in Stripe** on **Credits & Billing**, update the card, and auto-recharge tries again at the next hourly check while the balance is still below the trigger. --- Source: https://docs.versionseven.ai/help/failed-payments # Failed payments What pauses when a subscription payment fails, how long your senders are kept, and how to update your card to resume. When a subscription payment fails, Victoria AI stops sending until the payment goes through. Nothing is deleted right away. ## What you see when a payment fails - A red banner across the app: **Payment failed** or **Payment past due**, with **Update payment method**. - A **Payment failed** notification, and an email to the workspace owner. - On **Credits & Billing**, the plan's badge reads **Past due**. ## What pauses after a failed payment - **Sending stops.** No campaign sends, and the Appointment Setter pauses. - **No new senders.** You can't connect accounts. - **No activation.** Preflight fails **Active subscription** with "Payment failed. Update your card in Billing to resume sending." - **Auto-recharge stops.** Your campaigns, leads, sequences and credits stay as they are. ## How long senders are kept after a failed payment Your connected senders are kept for 21 days after the last failed payment while Stripe retries it. If the payment still hasn't gone through after that, the senders are disconnected and removed, and you'll need to connect them again after paying. ## Fix a failed payment 1. Select **Update payment method** on the banner, or go to **Credits & Billing**. 2. Select **Manage billing in Stripe**. Stripe's billing portal opens. 3. Update your card, and pay the open invoice if the portal offers it. When the payment goes through, Victoria AI clears the pause on its own and sending resumes. You don't need to turn campaigns back on. If the banner is still there after the payment went through, refresh the page. If it stays, contact us from **Feedback & Support**. --- Source: https://docs.versionseven.ai/help/cancel-or-reactivate # Cancel or reactivate your subscription How canceling works for a trial and a paid plan, what happens to your senders and credits, and how to reactivate. You can cancel from **Credits & Billing** at any time. Canceling a trial ends it at once; canceling a paid plan ends it at the end of the period you've paid for. ## Cancel a paid subscription 1. Go to **Credits & Billing** and scroll to **Danger Zone**. 2. Select **Cancel Subscription**. 3. Choose a **Reason for cancelling** and, optionally, add a note. 4. For some reasons, a **Before you cancel…** step offers a discount on your next invoices. Select **Yes, apply my discount** to stay, or **No thanks, cancel anyway**. If you've used this discount before, it isn't offered again. 5. On **Confirm cancellation**, select **Cancel subscription**. The plan badge changes to **Canceling**, and you keep full access until the end of the current billing period. While it's canceling, you can't connect new senders or activate campaigns, but active campaigns keep running. Only owners and admins can cancel or reactivate. ## Cancel a free trial 1. Go to **Credits & Billing → Danger Zone** and select **Cancel Trial**. 2. Choose a reason, then on **Confirm cancellation** select **Cancel trial**. Canceling a trial ends it now, not on its scheduled end date, and your card isn't charged. The toast reads "Your trial has ended and your card was not charged." ## What happens when a subscription ends When the trial is canceled, or a paid subscription reaches the end of its period: - All campaigns pause. - Connected senders are removed. - Unused plan credits, including trial and setup credits, are forfeited. - Purchased credits stay yours. ## Reactivate a subscription that's set to cancel While a paid subscription is still **Canceling**, select **Reactivate** on **Credits & Billing** (next to the plan or in the **Subscription is set to cancel** box). The cancellation is undone, billing continues as normal, and you can connect senders and activate campaigns again. ## Subscribe again after a subscription ended After a subscription or trial has ended, **Credits & Billing** shows **Subscription canceled** or **Trial ended**. Select the subscribe button to start a new Workspace plan. It's a paid checkout, because each workspace gets one trial. Then reconnect your senders and turn your campaigns back on. See [Connect LinkedIn](https://docs.versionseven.ai/help/connect-linkedin) and [Connect Gmail or Outlook](https://docs.versionseven.ai/help/connect-gmail-or-outlook). --- Source: https://docs.versionseven.ai/help/facts # Plans, limits and credits at a glance Every plan price, credit cost, trial limit, sending cap and setting bound in Victoria AI, on one page. The numbers on this page are read from the same source as the rest of the Help Center, so when one changes, every page that quotes it changes too. Monthly prices are in US dollars. 1 credit is 1,000 tokens. ## Plans Billing is monthly. Workspace takes extra LinkedIn seats and mailboxes as add-ons; Agency is a fixed bundle. | Item | Value | | - | - | | Workspace plan, per month | $99 | | LinkedIn seats included in Workspace | 1 | | Mailboxes included in Workspace | 3 | | Credits granted each Workspace billing cycle | 1,500 credits | | Agency plan, per month | $999 | | LinkedIn seats included in Agency (fixed, no add-ons) | 10 | | Mailboxes included in Agency (fixed, no add-ons) | 20 | | Credits granted each Agency billing cycle | 6,000 credits | ## Add-ons | Item | Value | | - | - | | Extra LinkedIn seat on Workspace, per month | $59 | | Extra credits each cycle per added LinkedIn seat | 500 credits | | Extra mailbox on Workspace, per month | $12 | | Extra credits each cycle per added mailbox | 150 credits | ## Credits Unused plan credits carry forward 1 billing cycle, and a balance holds at most 2 times the monthly grant. Purchased credits never expire. | Item | Value | | - | - | | Tokens in one credit | 1,000 | | Billing cycles unused plan credits carry forward | 1 | | Most plan credits a balance can hold, in monthly grants | 2 times | | Price of 1,000 credits bought as a pack | $25 | | Smallest credit purchase | $5 | ## What spends credits Costs marked "About" are averages metered from how much the AI reads and writes. Lead database results and found emails cost a fixed amount; a search page that comes back short, and an email that isn't found, are refunded. | Item | Value | | - | - | | One Lead database search result | 0.825 credits | | One work email found for a lead (charged only when found) | 3.3 credits | | Writing one lead's AI Personalization fields (average) | About 3 credits | | One AI Appointment Setter conversation (average) | About 1.5 credits | | Most leads one "Add matching leads" from the Lead database can add | 500 | | Results on one page of a Lead database search | 25 | | Most Lead database results you can tick and add at once | 100 | | Item | Value | | - | - | | Most the weekly campaign review spends per active campaign per week (its budget cap) | 10 credits | | Balance below which the weekly campaign review skips the week | 50 credits | ## Free trial | Item | Value | | - | - | | Free trial length | 30 days | | LinkedIn accounts during the trial | 1 | | Mailboxes during the trial | 1 | | Leads a trial workspace can hold | 250 | | Emails the trial can find in the Lead database | 100 | | Sends that end the trial early (billing starts) | 200 | | Appointment Setter conversations that end the trial early (billing starts) | 20 | | Credits granted once for the trial | 400 credits | | When the trial converts | The card is charged when the trial ends only if a sender was ever connected. A trial that never connected one ends without a charge. | | Lead database use allowed during the trial (searches and found emails together) | 330 credits | | How long before the trial ends the ending notice is sent (Stripe's trial\_will\_end) | 3 days | ## Setup bonuses Each bonus is paid once per workspace, when the step is reached. | Item | Value | | - | - | | Bonus for completing your profile | 50 credits | | Bonus for adding leads | 50 credits | | Bonus for building a sequence | 50 credits | | Bonus for connecting a sender | 50 credits | | Bonus for activating a first campaign (50 plus a 100 launch bonus) | 150 credits | | Bonus for a first reply | 100 credits | | All setup bonuses together, each paid once per workspace | 450 credits | ## Sending limits Limits are per sender account unless they say per campaign. A restriction from LinkedIn halves the account's LinkedIn caps for 7 days. | Item | Value | | - | - | | LinkedIn invites per account per day, and LinkedIn messages per account per day | 20 | | LinkedIn daily ceiling after an owner accepts the higher-limit risk | 30 | | LinkedIn connection requests per account per week | 100 | | LinkedIn weekly connection requests after an owner accepts the higher-limit risk | 200 | | LinkedIn messages per account per week | 200 | | LinkedIn profile views per account per week | 200 | | Emails per campaign per day (default daily limit) | 30 | | Emails per mailbox per week | 500 | | Percent of a mailbox's recent emails to unreachable addresses above which its sequence emails pause | 8 | | Window the bounce rate is measured over | 7 days | | Emails a mailbox must have sent in the window before the bounce throttle applies | 50 | | LinkedIn profile views per campaign per day (default daily limit) | 30 | | Warm-up ramp for a newly connected sender | 14 days | | LinkedIn invites or messages per day, days 0-2 of the ramp | 5 | | LinkedIn invites or messages per day, days 3-6 of the ramp | 10 | | LinkedIn invites or messages per day, days 7-13 of the ramp | 15 | | Emails per day, days 0-2 of the ramp | 5 | | Emails per day, days 3-6 of the ramp | 10 | | Emails per day, days 7-13 of the ramp | 20 | | How long a LinkedIn restriction halves the account's LinkedIn caps | 7 days | | How long a sender invite link works | 7 days | ## Campaigns and sequences | Item | Value | | - | - | | Default work hours for a new campaign | 9 AM to 5 PM, Monday to Friday, US Eastern time | | Highest daily limit a campaign accepts for emails and profile views | 500 | | Item | Value | | - | - | | Longest LinkedIn connection note | 300 characters | | Most steps in one sequence, across both variations and all branches | 40 | | Longest wait on one step | 90 days | | Extra connection checks when the No path is empty and the invite isn't accepted | 3 | | Wait between those connection checks | 24 hours | | Wait after the connection request in the built-in templates (the acceptance window) | 5 days | | Wait between the connection check and the first step on either path in the built-in templates | 5 days | | Item | Value | | - | - | | Percent of leads missing a variable (or an email, on an email-only sequence) above which activation is blocked | 10 | | Age after which a mailbox's DNS result is checked again at preflight | 24 hours | ## Leads and the inbox | Item | Value | | - | - | | How long a lead can stay due without its step going out before it's taken out of the sequence | 7 days | | Item | Value | | - | - | | Most Do Not Contact entries one paste adds | 500 | | Item | Value | | - | - | | Attachment size limit: per file on LinkedIn, per message in total on email | 25 MB | ## Personalization and the Appointment Setter | Item | Value | | - | - | | Longest fallback value for an AI Personalization field | 400 characters | | How long a lead is held when its AI field has nothing usable, before a retry | 24 hours | | Item | Value | | - | - | | Default wait before the Appointment Setter replies | 10 minutes | | Default Appointment Setter replies per conversation before it hands off to a person | 12 | | Most times the Appointment Setter shares the destination link in one conversation | 2 times | | Least wait after an out-of-office reply before the next touch | 7 days | | Longest the Appointment Setter waits when a prospect names a later time, before it checks back in | 30 days | | Default follow-ups when a prospect goes quiet | 5 | | Most follow-ups the setting accepts | 10 | | Default spacing of the follow-ups, in days after the last message | 2, 3, 5, 7, 10 | | Longest reply delay the setting accepts | 240 minutes | | Most agent replies per conversation the setting accepts | 40 | ## A/B tests | Item | Value | | - | - | | Leads each A/B variation needs before a winner is called | 50 | | p-value below which an A/B arm is called the winner | 0.05 | | p-value below which an A/B arm is shown as leaning ahead | 0.2 | ## Billing and auto-recharge | Item | Value | | - | - | | How long a past-due workspace keeps its connected senders while the payment is retried | 21 days | | Item | Value | | - | - | | Smallest auto-recharge amount | $5 | | Largest auto-recharge amount | $500 | | Most auto-recharges in 24 hours | 3 | | Least time between two auto-recharges | 1 hour | --- Source: https://docs.versionseven.ai/help/invite-teammates-and-roles # Invite teammates and manage roles Invite people to your Victoria AI workspace, what the owner, admin and member roles can each do, and how to change or remove someone. Every plan includes unlimited users. Each person in a workspace has one of three roles: owner, admin or member. ## Invite a teammate 1. Go to **Team** and select **Invite Member**. 2. Enter their **Email address** and choose a role: **Admin** or **Member**. 3. Select **Send Invite**. They get an email invitation. Until they accept, they appear on **Team** with a **Pending** badge. Owners and admins can send invites; a member who tries gets an error. To connect a teammate's LinkedIn or mailbox without giving them a login, send a sender invite link instead. See [Connect a sender with an invite link](https://docs.versionseven.ai/help/connect-a-sender-with-an-invite-link). ## What owners, admins and members can do Everyone in the workspace can build and run campaigns, add leads, use the Inbox, CRM and Copilot, connect senders, buy credits and add to the Do Not Contact list. Owners and admins can also: - Invite teammates. - Change the plan's add-ons, cancel, reactivate and set up auto-recharge. See [Plans and add-ons](https://docs.versionseven.ai/help/plans-and-add-ons). - Create and revoke API keys. See [API keys](https://docs.versionseven.ai/help/api-keys). - Remove any sender. A member can remove only a sender they connected. See [Remove or replace a sender](https://docs.versionseven.ai/help/remove-or-replace-a-sender). - Skip a sender's warm-up ramp or raise its LinkedIn limits. See [LinkedIn restrictions and limits](https://docs.versionseven.ai/help/linkedin-restrictions-and-limits). - Delete leads from the workspace. - Remove entries from the Do Not Contact list. - Turn the weekly campaign review on or off in **Settings**. The owner is the person who created the workspace. On the **Team** page, only the owner can change roles, remove members and resend invites. The owner also receives the workspace's billing and account emails, such as trial notices, failed payments and disconnected senders. ## Change a teammate's role As the owner, go to **Team** and choose **Admin** or **Member** in the person's **Role** column. The change applies at once. The owner role can't be given to someone else from the app. ## Remove a teammate or resend an invite As the owner, go to **Team**: - **Remove**: select the trash icon on their row, then **Remove**. They lose access to the workspace at once. The owner can't be removed. - **Resend an invite**: select the send icon on a **Pending** row. Removing someone doesn't remove the senders they connected. Remove those separately on **Sender Accounts** if you need to. --- Source: https://docs.versionseven.ai/help/api-keys # Create and manage API keys Generate an API key with the access it needs and an expiry, use it with the REST API or MCP, and revoke it when you're done. An API key lets a script, Zapier, or an AI tool act in your workspace through the Victoria AI REST API or MCP server. Only owners and admins can create, see and revoke keys. ## Generate an API key 1. Go to **Settings → API Keys** and select **Generate Key**. 2. Enter a **Key name** you'll recognize later, such as "Zapier". 3. Under **Access**, set each area to **None**, **Read** or **Write**: **Leads**, **Campaigns**, **CRM** and **Sender accounts**. Write includes read. Pick at least one area. 4. Choose when it **Expires**: **Never**, **30 days**, **90 days** or **1 year**. 5. Select **Generate**. 6. Copy the key from the yellow box that warns it won't be shown again, then select **I've saved this key**. The key starts with `vk_`. Store it somewhere safe; Victoria AI can't show it again. Scopes and expiry can't be changed later, so generate a new key if you need different access. ## Choose API key access Give each key the least access it needs. A key that only reads campaign results needs **Campaigns: Read** and nothing else. A key that adds leads to a campaign needs **Leads: Write**. The full list of what each scope allows is in the API docs under [Authentication](https://docs.versionseven.ai/guides/authentication#scopes). ## Use an API key Send the key as a bearer token: `Authorization: Bearer vk_…`. The same key works for the REST API and for MCP hosts that take a header. See [Authentication](https://docs.versionseven.ai/guides/authentication) and [Connect your AI](https://docs.versionseven.ai/guides/connect-your-ai). Each key's row shows when it was created, when it was last used, its expiry and its scopes. ## Revoke an API key 1. In **Settings → API Keys**, select the trash icon on the key's row. 2. Confirm with **Revoke key**. Anything using the key (scripts, Zapier, an AI connector set up with it) stops working immediately. This can't be undone. ## API key stopped working - **The row says Expired**: the key passed its expiry date. Generate a new key and update the integration. - **The key was revoked**: generate a new one. - **A request is refused for missing scope**: the key doesn't have access to that area. Generate a key with the access the request needs. - **You can't see API Keys or Generate Key fails**: only owners and admins can manage keys. Ask one to create the key. --- Source: https://docs.versionseven.ai/help/notifications # Notifications and alert emails Where Victoria AI notifications appear, what they alert you to, who gets the matching emails, and how to clear them. Victoria AI tells you when something needs your attention, such as a disconnected sender or a failed payment. Notifications appear in the app for the whole workspace, and the most important ones are also emailed to the workspace owner. ## Where notifications appear Select **Notifications** in the sidebar. A badge on the bell shows how many are unread. The page groups them into **Today**, **This week** and **Older**, and you can filter by **All**, **Unread**, **Billing** or **System**. Most notifications carry an action, such as **Reconnect** or **Update payment method**. Select the notification to mark it read and go straight to the page that fixes it. ## What Victoria AI notifies you about Examples of what you'll see: - **Sender disconnected**: a sender needs to be reconnected. See [Reconnect a sender](https://docs.versionseven.ai/help/reconnect-a-sender). - **Payment failed**: a subscription payment didn't go through. See [Failed payments](https://docs.versionseven.ai/help/failed-payments). - **Trial ending**: a notice a few days before the trial ends, with what to do. See [Free trial](https://docs.versionseven.ai/help/free-trial). Banners at the top of the app cover the same billing states while they last. ## Who receives notification emails The workspace owner receives the emails that go with them, such as a disconnected sender, a failed payment, a low credit balance, a domain failing its DNS check, auto-recharge results and trial notices. Admins and members see the in-app notifications but don't get these emails. Account emails like these can't be unsubscribed from, because they're about your workspace's service. ## Mark notifications as read - Select a notification to mark it read. - Select **Mark all read** at the top of the page to clear every unread one. Most notifications are shared by the workspace, so marking one read marks it read for everyone. Some clear themselves when the problem is fixed: reconnecting a sender marks its **Sender disconnected** notification read. --- Source: https://docs.versionseven.ai/help/troubleshooting-campaign-wont-activate # Campaign won't activate Why the Activate Campaign button stays disabled in Victoria AI, and how to clear each kind of blocker. ## Campaign won't activate: the Activate Campaign button is disabled The readiness window lists at least one failed check. **Activate Campaign** stays disabled until every failed check passes; warnings alone don't disable it. **Fix:** read each red line, select its **Fix →** link, correct the problem, then turn the campaign switch on again to re-run the checks. [Preflight checks](https://docs.versionseven.ai/help/preflight-checks) explains every message. ## Campaign won't activate: the sequence checks fail The most common blockers are on the **Sequence** tab: an empty sequence, an email with no subject or body, a LinkedIn message with no text, a variable with a typo, or `{{double_braces}}`. With A/B testing on, Variation B is checked too. **Fix:** open **Sequence**, fill in every step, and use single braces such as `{first_name}`. Check Variation B with the **B** button. ## Campaign won't activate: the sender checks fail "No accounts assigned", "Sequence has LinkedIn steps but no LinkedIn account is assigned", "assigned accounts are no longer active", or an account "also active in" another campaign. **Fix:** open **Settings → Connected Accounts** and select accounts that cover every channel the sequence uses. Reconnect disconnected accounts on **Sender Accounts** ([Reconnect a sender](https://docs.versionseven.ai/help/reconnect-a-sender)), and pause the other campaign or pick a different account when one is already in use. ## Campaign won't activate: email domain check fails "failed the email authentication check" means the mailbox's domain is missing an SPF or DMARC record, or has a broken one. **Fix:** correct the DNS record named in the message with your domain provider, then re-run the check on **Sender Accounts**. See [DNS setup](https://docs.versionseven.ai/help/dns-setup). ## Campaign won't activate: lead checks fail "No leads enrolled", too many leads missing data for a variable, or too many leads the sequence can't reach (no email for an email-only sequence, no LinkedIn URL for a LinkedIn-only one). **Fix:** add leads on **Leads**. For missing data, enrich or re-upload the leads, remove them, or remove the variable. For unreachable leads, add steps on the missing channel or remove those leads. ## Campaign won't activate: subscription or billing check fails "Payment failed", "Trial has ended", "Subscription is canceled", "set to cancel at period end", or more active accounts than your plan covers. **Fix:** open **Credits & Billing** and update the card, reactivate the plan, or add seats. See [Failed payments](https://docs.versionseven.ai/help/failed-payments) and [Free trial](https://docs.versionseven.ai/help/free-trial). ## Campaign won't activate: "Couldn't check readiness" The check didn't complete, so nothing was activated. **Fix:** select **Retry**. If it fails again, close the window, wait a minute and turn the switch on again. ## Campaign won't activate from the Copilot or an AI assistant The Copilot and connected assistants use the same checks. They answer with the failing checks instead of activating, and they ask before activating with warnings. **Fix:** ask the assistant which checks failed, fix them as above, and ask it to activate again. --- Source: https://docs.versionseven.ai/help/troubleshooting-leads-not-moving # Leads aren't moving Why leads in an active Victoria AI campaign aren't getting their next step, and the fix for each cause. Start on **Campaigns → your campaign**. If a red banner says **This campaign isn't sending**, fix what it lists first; see [Campaign health indicators](https://docs.versionseven.ai/help/campaign-health). If there's no banner, work through the causes below. You can also ask the AI Sales Copilot "why isn't this campaign sending?". It reads the campaign, its queue and its senders together and says what it finds. ## Leads not moving: the campaign is paused The switch at the top of the campaign shows **Paused**. Nothing sends from a paused campaign. **Fix:** turn the switch on. The readiness check runs first. See [Preflight checks](https://docs.versionseven.ai/help/preflight-checks). ## Leads not moving: a sender is disconnected Leads are assigned to a specific sender. If that account disconnected, its leads stop until it's back. **Fix:** reconnect the account on **Sender Accounts**. See [Reconnect a sender](https://docs.versionseven.ai/help/reconnect-a-sender). ## Leads not moving: a sender was removed from the campaign Leads already assigned to an account you unassigned in **Settings → Connected Accounts** stop moving. They aren't handed to another sender. **Fix:** select the account again in **Settings → Connected Accounts**. ## Leads not moving: outside work hours Steps only go out inside the campaign's work hours, in its timezone. A campaign whose **Work Hours** switch is off still sends only 9 AM to 5 PM, Monday to Friday, US Eastern time, not around the clock. **Fix:** check **Settings → Work Hours** and the timezone. Keep the switch on and turn on the days and hours you want. See [Work hours and daily limits](https://docs.versionseven.ai/help/work-hours-and-daily-limits). ## Leads not moving: daily limits or sender caps are used up Each action has a campaign daily limit per sender, and each account has its own daily and weekly caps. A new account ramps up over 14 days, starting low. When the caps are used, remaining leads wait for the next day. **Fix:** wait for the next day, raise the campaign's **Daily Limits**, or assign more sender accounts. See [Sender limits and ramp](https://docs.versionseven.ai/help/sender-limits-and-ramp). ## Leads not moving: the wait between steps hasn't passed Each step waits its **Wait after this step** before the next, counted from when the previous step went out. After a connection request, the lead waits the whole acceptance window before the branch check. **Fix:** none needed. Check the step waits on **Sequence**; the day labels show the earliest each step can go out. ## Leads not moving: no leads left to start **Not yet contacted** is zero on **Analytics**, and every lead is active, completed or paused. The campaign has nothing new to send. **Fix:** add leads on **Leads** with **Find Leads**, **Add Lead** or **Upload CSV**. ## Leads not moving: the credit balance is zero Sending stops when your credit balance runs out, because personalization and replies spend credits. **Fix:** add credits on **Credits & Billing**. See [Credits and rollover](https://docs.versionseven.ai/help/credits-and-rollover). ## Leads not moving: a payment failed After a failed payment, sending is paused for the whole workspace until the card is updated. **Fix:** update the card on **Credits & Billing**. See [Failed payments](https://docs.versionseven.ai/help/failed-payments). ## Leads not moving: the lead replied A lead who replied leaves the sequence, and a lead whose reply is still being processed is held so no step goes out on top of it. Their replies appear in the **Inbox**. **Fix:** none needed. Answer them in the **Inbox**, or let the Appointment Setter handle them. ## Leads not moving: an AI Personalization field is held When research can't support an AI field and it has no fallback that works, the lead is held for 24 hours and retried. **Fix:** add a **Fallback value** to the field. Use **Preview → Generate** on the step to see which leads would be held. See [Personalization fields](https://docs.versionseven.ai/help/personalization-fields). ## Leads not moving: the lead is in another campaign or on Do Not Contact A lead that's already in progress, or mid-conversation, in another campaign on the same channel waits rather than getting a second cold message. A lead on the Do Not Contact list is never contacted. **Fix:** check the lead's other campaigns, and the list in **Settings**. See [Do Not Contact](https://docs.versionseven.ai/help/do-not-contact). ## Leads not moving for a week A lead in an active campaign that has been due for 7 days without its step going out is taken out of the sequence, and its activity records it as paused for inactivity. This catches leads stuck behind a problem that wasn't fixed. **Fix:** fix the underlying cause above first. If many leads were affected, contact support with the campaign name. --- Source: https://docs.versionseven.ai/help/troubleshooting-no-replies # Campaign gets no replies Diagnose a Victoria AI campaign that sends but gets few or no replies, from deliverability to copy and targeting. First check that the campaign is actually sending. On **Campaigns → your campaign → Analytics**, **Contacted** should be growing. If it isn't, see [Leads aren't moving](https://docs.versionseven.ai/help/troubleshooting-leads-not-moving). If it is, work through deliverability, then copy, then targeting. Give a campaign enough volume before judging it: a reply rate on a few dozen contacted leads changes a lot with one reply. [Outbound benchmarks](https://docs.versionseven.ai/playbook/benchmarks) has typical ranges, and [Diagnose a campaign by its bottleneck](https://docs.versionseven.ai/playbook/diagnose-a-campaign) has the full checklist. ## No replies: emails may not be reaching the inbox Signs: plenty of **Emails sent**, almost no replies on email while LinkedIn gets some. - **Domain authentication**: on **Sender Accounts**, check the mailbox's SPF, DKIM and DMARC result. A failing domain is often filtered as spam. See [DNS setup](https://docs.versionseven.ai/help/dns-setup). - **Bounces**: a mailbox whose recent emails often go to dead addresses is throttled automatically. More than 8% of its last 7 days of emails going to unreachable addresses (once it has sent at least 50) pauses its sequence emails until the rate drops. Use verified emails, such as those found through the Lead database. - **Links and attachments**: links in sequence messages hurt deliverability and read as spam. Keep sequence messages free of links and attachments; let the Appointment Setter share a link when a prospect asks. - **Open rate** is a weak signal. Privacy features hide opens, so don't read a low open rate as proof of spam on its own. **Fix:** correct DNS, clean the lead list, and follow [Cold email principles](https://docs.versionseven.ai/playbook/cold-email-principles). ## No replies: LinkedIn requests aren't accepted Signs: low **Accept rate**, so few leads reach the **Yes** path where LinkedIn messages go out. - A connection note that pitches gets ignored. A blank request often does better. - Your LinkedIn profile is what the prospect sees first: photo, headline and summary. **Fix:** see [LinkedIn connection requests](https://docs.versionseven.ai/playbook/linkedin-connection-requests), and make sure the **No** path has emails so people who don't accept are still reached. ## No replies: the copy doesn't earn a reply Signs: messages are delivered and connections accepted, but replies are rare or negative. - Openings about you instead of them. - Long messages with several ideas. - Asking for a meeting in the first message. - Claims the prospect can't check. **Fix:** rewrite with the [Playbook](https://docs.versionseven.ai/playbook/cold-email-principles): one idea per message, a question as the first ask, short first-person sentences. Test a new angle against the current one with [A/B testing](https://docs.versionseven.ai/help/ab-testing). ## No replies: the leads are the wrong audience Signs: replies that say "not relevant" or "wrong person", or silence across every channel and copy variation. **Fix:** tighten the lead criteria: titles, seniority, company size and industry. See [Find leads in the Lead database](https://docs.versionseven.ai/help/find-leads-in-the-lead-database). Make sure the offer in the copy matches a problem those people own; see [Value proposition](https://docs.versionseven.ai/playbook/value-proposition). ## Replies arrive but no meetings are booked Signs: **Positive replies** but few **Meetings booked**. - Check that the Appointment Setter is on, in **Autonomous** mode, with a working **Booking link**. - Read the conversations in the **Inbox** for questions the agent couldn't answer, and add those facts to its **Knowledge base**. - Remember **Meetings booked** counts only confirmed bookings. **Fix:** see [Appointment Setter](https://docs.versionseven.ai/help/appointment-setter), [After a positive reply](https://docs.versionseven.ai/playbook/after-a-positive-reply) and [Appointment Setter configuration](https://docs.versionseven.ai/playbook/appointment-setter-configuration). --- Source: https://docs.versionseven.ai/help/troubleshooting-sender-disconnected # Sender disconnected What happens to a Victoria AI campaign when a LinkedIn or email sender disconnects, and how to get it sending again. ## What a disconnected sender does to a campaign A sender disconnects when LinkedIn or your mail provider ends the session, for example after a password change or a security check. While it's disconnected: - leads assigned to it stop moving; they aren't handed to another sender; - the campaign shows **This campaign isn't sending** with "assigned accounts are no longer active"; - the Dashboard lists it under sender accounts disconnected; - replies on its conversations can't be sent from the **Inbox**. ## Reconnect a disconnected sender **Fix:** open **Sender Accounts**, find the account and reconnect it. Reconnecting signs the same account in again and keeps its leads and conversations; it doesn't use a new seat. The step-by-step is in [Reconnect a sender](https://docs.versionseven.ai/help/reconnect-a-sender). Once it's back, its leads continue from where they stopped, within work hours and limits. You can also start from the **Fix →** link on the campaign banner, the **Dashboard** attention panel, or the reconnect prompt in the Inbox composer. ## A sender keeps disconnecting Repeated disconnects on LinkedIn usually come from a restriction or a security checkpoint on the account. **Fix:** see [LinkedIn restrictions and limits](https://docs.versionseven.ai/help/linkedin-restrictions-and-limits), and sign in to LinkedIn directly to clear any prompt it shows you. --- Source: https://docs.versionseven.ai/playbook # Playbook How to run outbound with Victoria AI: targeting, messaging, replies, and example sequences you can copy. ## Getting started - [Optimize your LinkedIn profile before you send](https://docs.versionseven.ai/playbook/linkedin-profile): The headshot and headline that get your Victoria AI connection requests accepted, and why your profile matters more than any message. ## Offer - [Give before you ask](https://docs.versionseven.ai/playbook/give-before-you-ask): Why cold outreach that gives value first beats asking, ideas for what to give, and how to give without sending content nobody asked for. - [Calls to action in cold outreach](https://docs.versionseven.ai/playbook/ctas): How to end a cold message in a Victoria AI sequence so it's easy to answer, starting with give-first CTAs and changing from first touch to last. - [Framing your value proposition](https://docs.versionseven.ai/playbook/value-proposition): How to state your offer in Victoria AI outreach as "I help X do Y by Z": a specific buyer, a niche outcome and a short differentiator. ## Targeting - [Who to target and on which channel](https://docs.versionseven.ai/playbook/targeting-and-channels): Which buyers respond on LinkedIn and which on email, and how to reach 1 to 3 decision makers at the same company without tripping over yourself. ## Copywriting - [Openers that get replies](https://docs.versionseven.ai/playbook/openers): Two opener structures for cold LinkedIn messages and emails in Victoria AI: "I help X do Y by Z" and the question opener, each with a give-first CTA. - [Cold email principles](https://docs.versionseven.ai/playbook/cold-email-principles): How to write cold emails in Victoria AI that get read and answered: short, about the prospect, giving before asking, one small CTA. - [LinkedIn connection requests](https://docs.versionseven.ai/playbook/linkedin-connection-requests): Why Victoria AI connection requests go out blank by default, how much that lifts acceptance, and what a note should say if you add one. - [LinkedIn messages after connecting](https://docs.versionseven.ai/playbook/linkedin-messages-after-connect): What to send on LinkedIn after a prospect accepts your Victoria AI connection request, message by message, using a give-first opener. - [Slop: words and habits that kill replies](https://docs.versionseven.ai/playbook/slop-language): The AI tells, played-out phrases and ask-ask-ask habits that make cold outreach read as spam, and what to write instead of each. - [Personalization that works](https://docs.versionseven.ai/playbook/personalization-instructions): The personalization that gets replies, in order: buying signals, offer-relevant company and role details, then personal details, with AI field instructions. - [Message formatting](https://docs.versionseven.ai/playbook/message-formatting): How to lay out emails and LinkedIn messages in a Victoria AI sequence so they're easy to read on a phone. ## Sequence strategy - [Step delays](https://docs.versionseven.ai/playbook/step-delays): How to space the steps of a Victoria AI sequence, from the connection acceptance window to backing off between messages. - [Choosing a sequence shape](https://docs.versionseven.ai/playbook/choosing-a-sequence-shape): The three sequence shapes we recommend in Victoria AI, LinkedIn only, email only and Email + LinkedIn, and which buyers and lead data each one suits. - [Size a campaign to your sending capacity](https://docs.versionseven.ai/playbook/size-a-campaign): How many leads to load into a Victoria AI campaign, based on your senders and daily limits, with room for an A/B test and a refill rhythm. ## Replies - [Configuring the Appointment Setter](https://docs.versionseven.ai/playbook/appointment-setter-configuration): How to set up and test the Victoria AI Appointment Setter so it works every reply toward your goal and hands the right conversations to you. - [Appointment Setter knowledge and assets](https://docs.versionseven.ai/playbook/setter-knowledge-documents): What to put in the Victoria AI Appointment Setter's campaign description, knowledge base and assets so its answers are accurate. - [After a positive reply](https://docs.versionseven.ai/playbook/after-a-positive-reply): What to send once a prospect replies with interest in Victoria AI, give first and then ask for the meeting, framed as a valuable next step. - [Objections and hard no's](https://docs.versionseven.ai/playbook/objections-and-hard-nos): How to answer a hard no with a friendly break-up, handle common objections in Victoria AI replies, and when a pricing question should become a call. ## Measuring - [Outbound benchmarks](https://docs.versionseven.ai/playbook/benchmarks): Typical cold outreach rates and the healthier targets to aim for with Victoria AI, mapped to its metrics, and how much data you need before judging. - [Diagnose a campaign by its bottleneck](https://docs.versionseven.ai/playbook/diagnose-a-campaign): A step-by-step checklist for finding why a Victoria AI campaign underperforms, starting at the first weak stage of the funnel and fixing that first. ## Example sequences - [Starter sequence: multichannel B2B](https://docs.versionseven.ai/playbook/sequences/multichannel-b2b-default): Our recommended Email + LinkedIn sequence in Victoria AI, a blank request with a 10-day window, then two branches that reference each other. - [Starter sequence: LinkedIn-only, give first](https://docs.versionseven.ai/playbook/sequences/linkedin-only-give-first): Our recommended LinkedIn-only sequence in Victoria AI, a blank connection request then three give-first messages, with full copy and timing. - [Starter sequence: email-only, give first](https://docs.versionseven.ai/playbook/sequences/email-only-give-first): Our recommended five-email sequence in Victoria AI, spaced 2, 3, 5 and 5 days apart, where every email gives something, with full copy. --- Source: https://docs.versionseven.ai/playbook/linkedin-profile # Optimize your LinkedIn profile before you send The headshot and headline that get your Victoria AI connection requests accepted, and why your profile matters more than any message. Victoria sends connection requests and messages from your own LinkedIn account, so your profile is the first thing every prospect judges. Fix it before you activate a campaign. ## Why your LinkedIn profile drives connection acceptance A prospect decides on a connection request in a second or two, and the request shows very little: your photo, your name and your headline. That's often all they see. When you send the request without a note, as we recommend, the profile is doing all of the work. An optimized profile is one of the biggest levers you have on acceptance. Every accepted request puts a lead on the **Yes** path of your sequence, where LinkedIn messages can reach them, so a better profile lifts everything that comes after it. ## A professional, clear LinkedIn headshot Your photo is the largest thing on the request. Make it easy to trust: - A clear, recent photo of your face, looking at the camera. - Good light and a plain or simple background. - Cropped so your face fills most of the frame; it shows small on a phone. - Dressed the way you would be for a call with this buyer. Skip logos, group photos, sunglasses, distant shots and no photo at all. A request from a profile without a real face reads as a bot or a spammer. ## A short, clear LinkedIn headline The headline sits right under your name on the connection request, so it's the line people actually read. Keep it short and sweet, and say who you help and with what. ```text Weak: Founder | CEO | Visionary | Speaker | Growth Hacker | Dad Weak: Helping businesses unlock their full potential through innovative solutions Better: I help SaaS founders get booked on podcasts in their niche Better: Founder at [company] · outbound for B2B agencies ``` A long headline gets cut off on the request, and a string of titles tells the prospect nothing. If they can't tell what you do in one glance, they're less likely to accept. ## The rest of your LinkedIn profile People who accept, or who are on the fence, often click through. Make that visit confirm the headline: - **Banner**: a simple image that repeats who you help, or a plain one. Avoid busy ads. - **About**: a few short lines on who you help, the problem you solve and how to reach you. Write it in the first person. - **Recent activity**: a post or comment in the last few weeks shows a real, active person. - **Experience**: your current role filled in, matching the company in your messages. ## Check your LinkedIn profile on a phone Open a connection request you've received on your phone and look at how little shows. Then look at your own profile the same way: is the photo clear at that size, and does the headline make sense before it's cut off? If not, fix those two first. See [LinkedIn connection requests](https://docs.versionseven.ai/playbook/linkedin-connection-requests) for what to send with the request. --- Source: https://docs.versionseven.ai/playbook/give-before-you-ask # Give before you ask Why cold outreach that gives value first beats asking, ideas for what to give, and how to give without sending content nobody asked for. ## Give, give, give: why giving beats asking A cold prospect owes you nothing. Every message that asks for something, a call, a demo, their time, spends trust you haven't earned yet. A message that gives them something useful earns it. So give as much as you can for free, and ask for little. Trust and a real will to help go a lot further than a clever ask, every time. The deal comes after they've seen you help. ## The ask, ask, ask habit in cold outreach The most common way outreach fails is a sequence where every message asks: 1. "Can we book 15 minutes?" 2. "Just checking if you saw my note about a call?" 3. "Do you have time this week?" Each one asks the prospect to do work for you, and nothing in it is for them. By the third ask, they've learned that every message from you is a request. Turn each ask into a give. Instead of "Can we book a call?", offer the thing the call would have given them. ## What to give in cold outreach Good gives are useful on their own, quick to receive and specific to the prospect: - **An intro**: to a partner, a customer, a host, a hire or a peer they'd want to know. "Mind if I make a quick intro?" - **A resource**: a short checklist, a template, a teardown or a one-page guide that solves a piece of their problem. - **A free sample**: a small piece of the work you sell, such as a few leads, one rewritten page or one design. - **A short audit**: a few specific observations about their site, listing, funnel or outreach, with one thing to fix. - **A useful idea**: one concrete thing that's working for companies like theirs, in two or three lines. Only offer what you can actually deliver. If you offer an intro or a sample, have it ready, and add any link you'd share to the Appointment Setter's **Assets** so it can share it when it's relevant. See [Appointment Setter knowledge and assets](https://docs.versionseven.ai/playbook/setter-knowledge-documents). ## Give in short messages, not newsletters A give is not permission to send long content. Don't sign someone up for a newsletter they didn't opt in for, and don't paste an article into a cold email. - Offer the give in a line or two and let them say yes: "Want me to send it over?" - If you share something in the message itself, keep it to a few lines. - Never add prospects to a mailing list from a cold sequence. A long message nobody asked for reads as marketing, and it's easy to ignore, delete or mark as spam. ## From give to deal Giving first doesn't mean never selling. The order is: give, follow up, then work the deal. 1. **Give**: the first touch offers something useful with a small yes. 2. **Follow up**: after they've received it, ask how it went or offer the next piece. 3. **Work the deal**: once they've seen the value, talk about doing more of it, in whatever way fits your business. When a prospect replies, the Appointment Setter takes over the conversation and can move it toward a meeting. See [After a positive reply](https://docs.versionseven.ai/playbook/after-a-positive-reply). If you'd rather have our team run outbound for you, including building something custom for each interested prospect, see our [managed program](https://www.versionseven.ai/proof-first-gtm). --- Source: https://docs.versionseven.ai/playbook/ctas # Calls to action in cold outreach How to end a cold message in a Victoria AI sequence so it's easy to answer, starting with give-first CTAs and changing from first touch to last. The call to action is the sentence that decides whether a prospect replies. Cold prospects owe you nothing, so the smaller the ask, the easier the yes. The easiest yes of all is to something you're giving them. ## Give-first CTAs: offer instead of ask If you can give instead of ask, do. A give-first CTA offers something useful and asks only for permission to send it: - "Mind if I make a quick intro with an interested \[host / partner / customer]?" - "Mind if I send over \[a short sample / a quick audit of your site]?" - "Want me to share the \[checklist / template] we use for this?" Saying yes costs the prospect nothing and gets them something they want, so it's the lowest-friction CTA there is. Once they've received the give, you follow up and work the deal. See [Give before you ask](https://docs.versionseven.ai/playbook/give-before-you-ask). Only offer what you can actually deliver, and have it ready. ## Cold outreach CTAs from lowest to highest friction 1. **A give-first offer**: "Mind if I make a quick intro?" 2. **A yes or no question**: "Is this on your list this quarter?" 3. **A single factual question**: "Who on your team handles outbound?" 4. **A soft meeting ask**: "Would a 15-minute call be worth it?" Use it once they've had value from you. 5. **A hard meeting ask**: "Can we book a call this week?" Save it for someone who has shown interest. ## Match the CTA to the step - **First touch**: a give-first offer, or a single question about them. No link and no meeting ask. - **Middle touches**: another give, or a question that follows from the last one. A soft meeting ask is fine once you've given something. - **Last touch**: direct, with the offer left open and an easy way out. ```text I've reached out a couple of times and don't want to be a nuisance. If the timing's off, just say so. The offer of [the give] stands whenever it's useful. ``` ## CTA mistakes to avoid - Asking in every message. An "ask, ask, ask" sequence teaches the prospect that you only want something. - More than one ask in a message. Pick one. - A meeting ask before you've given anything or said why it's relevant to them. - A link of any kind in a sequence message, including a booking link. It hurts deliverability and assumes a yes. Links go out only in Appointment Setter replies, when the prospect asks. ## Let the Appointment Setter make the booking ask Once a prospect replies with interest, the Appointment Setter takes over: it delivers the give, then asks for the meeting and shares your booking link. Your sequence doesn't need to push for the meeting; it needs to start the conversation, ideally by giving something. See [After a positive reply](https://docs.versionseven.ai/playbook/after-a-positive-reply). --- Source: https://docs.versionseven.ai/playbook/value-proposition # Framing your value proposition How to state your offer in Victoria AI outreach as "I help X do Y by Z": a specific buyer, a niche outcome and a short differentiator. ## The "I help X do Y by Z" value proposition State your value proposition in one sentence: ```text I help [X: a specific kind of company or person] [Y: solve a specific, niche problem or reach a specific, niche goal] by [Z: a very short differentiator]. ``` ```text I help growing SaaS founders grow their social media audiences by connecting them with podcast hosts in their niche. ``` - **X, who**: specific enough that the right prospect recognises themselves. "Growing SaaS founders", not "businesses". - **Y, what**: one outcome they care about, in their words. The more niche, the more believable. - **Z, how**: a few words on what makes your way different. Not a feature list. The same sentence is the first line of one of the two openers that work best for us. See [Openers that get replies](https://docs.versionseven.ai/playbook/openers). ## Lead with outcomes, not features Prospects care what changes for them, not how your product works. Y is an outcome; Z is only there to make it believable. ```text Feature: We offer an AI platform with multichannel sequencing and analytics. Outcome: I help small sales teams start more first conversations without hiring more SDRs. ``` ## Make value claims you can prove A specific, real result is believable; a vague or invented one costs you trust. Use a number, a client name or a result only when it's real and you could back it up. If you don't have proof yet, describe the change you make instead of inventing a figure, or soften it: "teams we work with often" rather than a percentage you can't show. The Copilot follows the same rule: no unverifiable claims in any message it writes. ## Give a reason for now Cold prospects have no urgency, so point to one that's real for them: a stage their company is at, a trend in their market, or a common pressure in their role. ```text Teams that just raised often find outbound is the first thing that has to scale. ``` ## Pair the value proposition with a give, not a demo Follow your one-line value proposition with something small you can give, not "Can we book a demo?": - "Mind if I make a quick intro?" - "I wrote up how we'd approach this for `{company}`. Want me to send it?" - "We have a short case study on a team like yours. Worth sharing?" Only offer what you actually have ready to send. See [Give before you ask](https://docs.versionseven.ai/playbook/give-before-you-ask). ## Put the value proposition into the Appointment Setter The Appointment Setter answers "what is this about?" from the **Campaign description**, and states facts only from its **Knowledge base**. Put your one-line value proposition and your real proof points there. See [Appointment Setter configuration](https://docs.versionseven.ai/playbook/appointment-setter-configuration). --- Source: https://docs.versionseven.ai/playbook/targeting-and-channels # Who to target and on which channel Which buyers respond on LinkedIn and which on email, and how to reach 1 to 3 decision makers at the same company without tripping over yourself. ## Most B2B buyers are a good fit for outbound Most B2B companies are great for outbound: there's a clear buyer, a business problem and a budget. If you can name the kind of company and the role you help, you can build a campaign for them. ## Most B2B buyers respond better on LinkedIn For most B2B companies, founders and executives, LinkedIn gets more responses than email. A connection request is a light first touch, an accepted request puts you in a smaller and less crowded inbox, and the prospect can see who you are before they answer. So for most B2B campaigns, lead with LinkedIn: a LinkedIn-only sequence, or an Email + LinkedIn sequence where LinkedIn comes first and email reaches the people who don't accept. See [Choosing a sequence shape](https://docs.versionseven.ai/playbook/choosing-a-sequence-shape). ## Home services businesses respond more on email and phone A few markets are the opposite of most B2B. Home services, such as contractors, HVAC, pool, roofing and landscaping companies, tend to respond more on email and phone than on LinkedIn. Owners in these trades are often on job sites, not on LinkedIn, and their inbox and phone are where the business runs. For these buyers, run an email-only sequence and lean on volume and a useful offer. Victoria AI sends email and LinkedIn; phone calls are yours to make, so add them to your own routine for this list. See the [email-only starter](https://docs.versionseven.ai/playbook/sequences/email-only-give-first). ## Target 1 to 3 decision makers at each company Most deals involve more than one person. We recommend reaching 1 to 3 stakeholders or decision makers at each company, for example the founder, the head of the team you help and the person who runs the day-to-day work. More than one contact raises the chance that someone answers, and a reply from one person gives you context for the others. More than three starts to feel like the whole company got the same blast. ## Keep multi-stakeholder conversations in context of each other Writing to several people at one company only works if each conversation fits with the others. Treat it as one account, not three strangers: - **Put them in the same campaign.** Their copy and timing are then written once, together, and you can see all of them in the campaign's **Leads** tab. - **Never send colleagues identical copy.** People at the same company compare notes. Vary the opener and the angle by role: the founder hears about the outcome, the team lead about the day-to-day problem. - **Reference colleagues carefully.** "I also reached out to your head of sales" is fine when it's true and helps; don't name people in a way that feels like going around someone. To name a colleague in a message, add a column such as `colleague_name` to your CSV, keep it as a custom field, and use it as `{colleague_name}`. The readiness check flags leads that have no value for it. See [Import a CSV](https://docs.versionseven.ai/help/import-a-csv). - **When one person replies, check the others.** A reply stops the sequence only for the person who replied; their colleagues keep getting their steps. If the reply changes things, open the colleagues in the campaign's **Leads** tab and select **Remove from campaign**, or carry on with a message that fits. - **If someone asks you to stop, respect it for the whole company when that's what they meant.** Add the company's domain to the [Do Not Contact list](https://docs.versionseven.ai/help/do-not-contact) and every address at that domain is skipped. --- Source: https://docs.versionseven.ai/playbook/openers # Openers that get replies Two opener structures for cold LinkedIn messages and emails in Victoria AI: "I help X do Y by Z" and the question opener, each with a give-first CTA. The opener is the first message a prospect reads from you: the first LinkedIn message after they accept, or the first email. Two structures work best for us. Both are short, both are about the prospect, and both end with one small call to action. ## The "I help X do Y by Z" opener Say who you help, what you help them do and how, in one sentence. Then ask one short question. ```text Hey {first_name}, I help [X: a specific kind of company or person] [Y: solve a specific, niche problem or reach a specific goal] by [Z: a very short differentiator]. [One short CTA. If you can give instead of ask, even better.] ``` - **X** is specific. "Growing SaaS founders" beats "businesses". The prospect should recognise themselves. - **Y** is niche. One problem or one goal, in their words, not a list of what your product does. - **Z** is a few words. Just enough to show you do it differently, not a feature tour. An example from our own outreach: ```text Hey Avery, I help growing SaaS founders grow their social media audiences by connecting them with podcast hosts in their niche. Mind if I make a quick intro with an interested host? ``` The CTA gives instead of asks. Saying yes costs the prospect nothing and gets them something they want. ## The question opener Open with something specific and true about them, then ask one question about it. ```text Hey {first_name}, [one specific, true observation about them]. [One question about it.] ``` An example from our own outreach: ```text Hey Avery, loved your podcast episode with [guest]. Are you scheduled for any more appearances this month? ``` The observation has to be real. In Victoria AI, it usually comes from an AI Personalization field, such as one that finds a recent episode, post or launch, with a fallback that still reads naturally. See [Personalization that works](https://docs.versionseven.ai/playbook/personalization-instructions). ## Give value first, then follow up and work the deal Both openers lead to the same strategy: give something of value first, then follow up, then work the deal however it makes sense for your company. In the podcast example, a yes to "Mind if I make a quick intro?" means you make the intro. That's the give. Once they've seen it work, you follow up and talk about doing more of it, as a paid service or whatever your offer is. This works because the first thing the prospect gets from you is help, not a request. See [Give before you ask](https://docs.versionseven.ai/playbook/give-before-you-ask). ## Fill-in opener templates Copy one of these into the first step and fill in the brackets: ```text Hey {first_name}, I help [kind of company or role] [do or fix one specific thing] by [a few words on how]. Mind if I [send / make / share] [the give: an intro, a sample, a short audit]? ``` ```text Hey {first_name}, [one specific, true thing about them or {company}]. [Are you / Is your team] [doing the thing your offer helps with] [this month / this quarter]? ``` Keep the opener to two or three short lines. If it doesn't fit on a phone screen without scrolling, cut it. ## Opener mistakes to avoid - Introducing yourself before saying who you help: "My name is…, I'm the founder of…". - A generic X: "I help businesses grow." - A long Z: a paragraph of features where a few words would do. - Two questions, or a question plus a meeting ask. - A compliment that isn't specific or isn't true. "Loved your post" with no detail reads as a template. - Played-out lines such as "I came across your profile". See [Slop](https://docs.versionseven.ai/playbook/slop-language). --- Source: https://docs.versionseven.ai/playbook/cold-email-principles # Cold email principles How to write cold emails in Victoria AI that get read and answered: short, about the prospect, giving before asking, one small CTA. ## Cold email: keep it short and about one thing A cold email is read on a phone, between other things. Write a few short sentences with one idea, in the first person, as one person writing to another. Cut anything that explains your whole product. No links and no attachments. ## Cold email: open with one of two structures The first line decides whether the rest gets read. Two openers work best for us: - **"I help X do Y by Z"**: who you help, the specific thing you help them do, and how in a few words. - **The question opener**: one specific, true observation about them, then one question about it. ```text Weak: Hi {first_name}, I'm reaching out because we offer a platform that helps companies like yours... Better: Hi {first_name}, I help [growing SaaS founders] [grow their audiences] by [connecting them with podcast hosts in their niche]. Better: Hi {first_name}, saw that {company} is hiring its first SDRs. Are you building outbound from scratch? ``` Only state what you know is true. If the observation comes from an AI Personalization field, give the field a fallback that still reads naturally. See [Openers that get replies](https://docs.versionseven.ai/playbook/openers). ## Cold email: give before you ask End the first email with a small offer, not a request for time. "Mind if I send over \[a short sample]?" is easier to say yes to than "Can we book a call?", and it starts the relationship with you helping. Across the sequence, give, give, give: each email offers something useful, such as an idea, a resource, an intro or a sample. Ask for little. See [Give before you ask](https://docs.versionseven.ai/playbook/give-before-you-ask). ## Cold email length: give without sending a newsletter Giving doesn't mean sending long content. Don't sign someone up for a newsletter they didn't opt in for, and don't paste an article into a cold email. - Offer the give in a line, and send it if they say yes. - If you share an idea in the email itself, keep it to two or three lines. - If an email is longer than a phone screen, cut it. ## Cold email subject lines Short and plain, a few words, often lowercase. Specific beats clever: "question about `{company}`" or "`{first_name}`, quick one". Avoid words that read like marketing, such as "opportunity" or "partnership". ## Cold email follow-ups Never resend the same email, and never send "just following up". Each follow-up gives something new: - **Middle emails**: a different give or a different angle, such as a new idea, a resource, or a question about whether you've reached the right person. - **Last email**: short and direct. Say it's the last note, leave the offer open and make "not now" an easy answer. ## Reference LinkedIn in a multichannel cold email In an Email + LinkedIn sequence, the lead also sees your LinkedIn request and messages. Say so in a few words, so the two channels read as one conversation: "I sent you a note on LinkedIn too, so I'll keep this short." Do it in every email after the first touch. See the [multichannel starter](https://docs.versionseven.ai/playbook/sequences/multichannel-b2b-default). ## Cold email rules the Copilot follows The AI Sales Copilot writes every sequence to these rules, and they hold for copy you write yourself: - No links and no attachments in any sequence message. They hurt deliverability and read as a pitch. A link goes out only in an Appointment Setter reply, when the prospect asks for it. - No claims you can't prove. Every number, client name or result must be real; soften anything uncertain, such as "teams we work with often" instead of a made-up percentage. - A small, give-first ask in early touches, not a meeting. - Short, first person, no em dashes and no slop. See [Slop](https://docs.versionseven.ai/playbook/slop-language). ## No links or attachments in cold email Never put a link or an attachment in a sequence email: not your website, not a case study, not a booking page. Mailbox providers treat links and attachments in cold email as spam signals, and in a first email they read as a pitch. Offer the give in words instead, and send it once they say yes. The Appointment Setter shares a link from your **Assets** or your booking link in its reply when the prospect asks. See [Configuring the Appointment Setter](https://docs.versionseven.ai/playbook/appointment-setter-configuration). ## Send cold email from a dedicated mailbox and domain Copy only matters if it reaches the inbox. Don't send cold email from the mailbox you use every day: use mailboxes on a dedicated sending domain, warm them up with a warm-up service, and set up SPF, DKIM and DMARC. See [Sending mailbox best practices](https://docs.versionseven.ai/help/sending-mailbox-best-practices). ## Cold email variables Variables use single braces: `{first_name}`, `{last_name}`, `{company}`, `{title}`, custom fields from your CSV, and AI Personalization fields by their exact name. Double braces don't work. See [Personalization fields](https://docs.versionseven.ai/help/personalization-fields). --- Source: https://docs.versionseven.ai/playbook/linkedin-connection-requests # LinkedIn connection requests Why Victoria AI connection requests go out blank by default, how much that lifts acceptance, and what a note should say if you add one. ## The job of a LinkedIn connection request A connection request has one job: get accepted. Acceptance puts the lead on the **Yes** path, where LinkedIn messages can reach them. Don't pitch in the request; a pitch gets ignored or declined. ## Send LinkedIn connection requests blank by default Send the invite with no message. In our testing, requests with no note were accepted 10% to 30% more often than requests with one. The likely reason: a blank request doesn't start the relationship on an angle or an ask. A note, however friendly, tells the prospect you want something. A blank request is just one professional connecting with another. The Copilot leaves the note empty unless you ask for one, and the built-in **Email + LinkedIn** and **LinkedIn Only** templates send a blank request. In the Sequence editor the note is marked **(optional)**, and an empty note is a valid step. ## Your profile does the work on a blank LinkedIn request With no note, the prospect decides from your photo, your name and your headline. A clear, professional headshot and a short headline about who you help do more for acceptance than any note. See [Optimize your LinkedIn profile](https://docs.versionseven.ai/playbook/linkedin-profile). ## If you add a LinkedIn connection note, keep it neutral If you'd rather send a message, make it a neutral peer note: no pitch, no ask, and a personalization field so it reads as written for them. ```text Hey {first_name}, I'm looking to connect with fellow podcast hosts. Would be great to chat sometime. ``` ```text Hey {first_name}, I'm connecting with other founders in [your market], and {company} came up. Would be good to know each other. ``` The first is an example from our own outreach. Swap "podcast hosts" and "founders" for the group you and the prospect both belong to. ## LinkedIn connection note rules - Up to 300 characters, counted after variables are filled in. Long company names or titles can push a note over, and LinkedIn refuses it. - Use only standard and custom fields, such as `{first_name}`, `{company}` or a column from your CSV. Don't put AI Personalization fields in a connection note; the editor doesn't offer them there. - No product, no company pitch, no link. - Avoid LinkedIn's default text and anything that reads like a template. ## Make the No path count Some leads never accept. In an Email + LinkedIn sequence, put emails on the **No** path so they're still reached. See [Choosing a sequence shape](https://docs.versionseven.ai/playbook/choosing-a-sequence-shape). --- Source: https://docs.versionseven.ai/playbook/linkedin-messages-after-connect # LinkedIn messages after connecting What to send on LinkedIn after a prospect accepts your Victoria AI connection request, message by message, using a give-first opener. LinkedIn messages go only to people who accepted, so they're warmer than a cold email. Use that: be brief, be useful and lead with something for them. ## First LinkedIn message after connecting The first message is the highest-impact touch in the sequence. Use one of two opener structures, then one short CTA that gives rather than asks. The "I help X do Y by Z" opener: ```text Hey {first_name}, I help [specific kind of company] [solve one specific problem] by [a very short differentiator]. Mind if I [make a quick intro / send a short sample / share a quick idea]? ``` The question opener, which needs something specific and true about them, usually from an AI Personalization field (here `{recent_highlight}`): ```text Hey {first_name}, {recent_highlight}. [One question about it, such as: are you doing more of that this month?] ``` No "thanks for connecting" padding, no pitch and no meeting ask. See [Openers that get replies](https://docs.versionseven.ai/playbook/openers) for both structures with examples. ## Second LinkedIn message Give something more. If they didn't answer the first offer, make it again in a different form, or give the thing outright in a line or two: one idea that's working for companies like theirs, or a resource they can use without replying. ```text {first_name}, one thing that's working for [companies like theirs] right now: [the idea, in one line]. Happy to send [the give] if it's useful. ``` ## Last LinkedIn message Keep the door open and leave the give on the table. Make it easy to say "later". ```text Last note from me, {first_name}. If the timing's off, no problem at all. The offer of [the give] stands whenever it's useful. ``` ## Reference email in a multichannel LinkedIn message In an Email + LinkedIn sequence, the lead also gets your emails. Mention them, so the two channels read as one conversation instead of two strangers writing: ```text {first_name}, I sent you an email about [the give] as well. Easier to reply here if you prefer. ``` Every touch after the first should tie back to the other channel in a few words. See the [multichannel starter](https://docs.versionseven.ai/playbook/sequences/multichannel-b2b-default). ## LinkedIn message style - Write like a person. Read it out loud; if it sounds like a brochure, rewrite it. - Shorter is better on LinkedIn: two or three short lines. - No bullets or bold. Use short paragraphs with blank lines between them. - No links and no attachments. Offer the give in words; the Appointment Setter shares a link when the prospect asks for it. - One AI Personalization field at most, and never two in one message. - No slop: no em dashes, no "I came across your profile". See [Slop](https://docs.versionseven.ai/playbook/slop-language). --- Source: https://docs.versionseven.ai/playbook/slop-language # Slop: words and habits that kill replies The AI tells, played-out phrases and ask-ask-ask habits that make cold outreach read as spam, and what to write instead of each. ## What slop is in cold outreach Slop is language that tells the reader nobody really wrote this for them. It's the phrasing of AI tools, of templates everyone has seen, and of salespeople who only ask. Prospects spot it in a line or two and stop reading. There are three kinds: AI slop, played-out phrases and the ask, ask, ask habit. Our team flags the first items in each list below in every sequence we review; the rest are other common ones to cut. ## AI slop: words that mark a message as machine-written Our team's list: - **Em dashes** (the long dash). Use a comma, a full stop or two sentences. - **"Genuinely"**. Delete the word; the sentence is stronger without it. Other common AI tells: | Slop | Write instead | | - | - | | "truly", "really", "incredibly" used for emphasis | delete the word | | "delve into" | "look at", or say the thing | | "leverage" | "use" | | "game-changer" | what actually changes, in plain words | | "In today's fast-paced world…" | start with the point | | "seamless", "robust", "cutting-edge" | the specific result | | "unlock", "elevate", "empower" | the plain verb: "get", "improve", "help" | | "I'd love to explore synergies" | one specific idea for them | | Lists of three adjectives | one adjective, or none | The AI Sales Copilot writes sequences without these. Check your own edits and any copy you paste in. ## Overly formal and played-out phrases in cold outreach Our team's list: - **"Hope this email finds you well"** (or "this message"), and other formal openers like it. Start with their name and the point. Other played-out phrases: | Slop | Write instead | | - | - | | "I came across your profile" | the specific thing you noticed | | "I wanted to reach out" | just say why you're writing | | "My name is … and I'm the … at …" | "I help \[who] \[do what] by \[how]" | | "Just following up" | a new reason to reply, or a new give | | "Circling back" / "bumping this to the top of your inbox" | a new reason to reply | | "Touch base" | say what you'd talk about | | "Quick call to pick your brain" | a specific question they can answer in a reply | | "Dear Sir or Madam", "To whom it may concern" | `{first_name}` | | "Please do not hesitate to contact me" | one short question | | "Best regards" plus a long signature | your first name | Write the way you'd message a peer you respect: short, plain and specific. See [Openers that get replies](https://docs.versionseven.ai/playbook/openers) for two structures that work. ## The ask, ask, ask habit From our team: don't take an "ask, ask, ask" approach. A sequence where every message asks for a call, a demo or a reply trains the prospect to ignore you. It's better to provide as much as you can for free; trust and a real will to help go a lot further, always. What it looks like, and what to do instead: - **"Can we book 15 minutes?" in the first message.** Offer something useful instead: "Mind if I make a quick intro?" - **Every follow-up repeats the ask.** Give something new in each one: an idea, a resource, a sample. - **A booking link before they've shown interest.** Let the Appointment Setter share it once they reply with interest. - **Asking for referrals from someone who hasn't replied.** Earn the conversation first. See [Give before you ask](https://docs.versionseven.ai/playbook/give-before-you-ask). ## Slop check before you activate a campaign Read every step out loud before you activate. Cut: - Any em dash. - Any word you'd never say to a person across a table. - Any opener that could be sent, unchanged, to anyone on earth. - Any follow-up that only asks again. If a message still sounds like a template after that, rewrite it from the prospect's side: what do they get from reading it? --- Source: https://docs.versionseven.ai/playbook/personalization-instructions # Personalization that works The personalization that gets replies, in order: buying signals, offer-relevant company and role details, then personal details, with AI field instructions. Personalization earns a reply when it points at a problem the prospect has and you can solve. Not all personalization is equal. In order of what works best: a buying signal, then a company or role detail tied to your offer, then a personal detail from their profile, which you can usually skip. ## The personalization hierarchy for cold outreach 1. **A buying signal**: something happening at the company right now that creates the problem you solve. A message that zeroes in on solving that specific, real problem is the best message you can send. 2. **A company or role detail tied to your offer**: who they serve, what they sell, the market they're in, or a priority common to their role, connected to the value you give. 3. **A personal detail from their profile**: their school, a hobby, a post unrelated to your offer. This often does more harm than good. It's fine to leave it out. Use the highest tier you have true data for. When you have nothing true at a tier, drop to the one below. Never invent a detail to fill the gap. ## Personalization tier 1: a buying signal tied to a real problem A buying signal is a recent, visible event that makes your offer relevant now. Common ones: - Hiring for a role your offer supports, such as a first SDR or a third support hire. - A launch, a new market or a new location. - A change in how the company sells, such as adding a sales team or a free trial. - The person posting about the problem you solve. Name the signal, then name the problem it usually creates, then offer to help with that problem. Here `{signal_line}` is a column from your lead file, such as "is hiring its first SDRs": ```text Hey {first_name}, saw that {company} {signal_line}. The first month is usually spent building lists by hand. Mind if I send over [the give: the list we'd start with for a team like yours]? ``` The signal must be true and recent. A stale or wrong signal reads worse than no personalization at all. ## Find buying signals with Victoria Pulse [Victoria Pulse](https://www.versionseven.ai/pulse) is VersionSeven's separate signal product. It tracks the companies in your market and flags buying signals, such as a jump in hiring, new positioning or a change in how a company sells, with a source for every fact and the dates each signal is active. To use a signal in a Victoria AI campaign: 1. Add it to your lead file as its own column, written to fit a sentence, such as `signal_line` with "is hiring its first SDRs". 2. When you upload the file, keep the column as a custom field. See [Import a CSV](https://docs.versionseven.ai/help/import-a-csv). 3. Use it in a message as `{signal_line}`. The readiness check flags leads that have no value for it. Reach out while the signal is active. A hiring push from six months ago is no longer a reason to write. Without Pulse, an AI Personalization field can look for signals on the company website and in the person's recent LinkedIn posts. See "AI Personalization instructions for a buying signal" below. ## Personalization tier 2: company or role details tied to your offer When there's no signal, use a detail about the company or the role that leads straight to the value you give: the customers they serve, a service they sell, the market they're in, or a priority common to people in their role. ```text Weak: Hey Sam, love what Northside is doing in the HVAC space. Better: Hey Sam, saw that Northside does commercial installs as well as residential. I help HVAC companies fill the slow months with commercial maintenance contracts. ``` The test: does the detail explain why you're writing to this person? If it could be deleted without changing the message, it's decoration, not personalization. ## Personalization tier 3: personal profile details, usually skip them A random personal detail, such as where they studied, a hobby, a sports team or a post about something unrelated to your offer, is the weakest kind of personalization. In many cases it does more harm than good: - It reads like a template trick, because everyone has seen "Saw you went to \[school]!". - It can feel like surveillance. - It says nothing about why you're writing, so the prospect still has to work that out. It's fine to leave personal details out entirely. The one exception is a personal post about the problem you solve, which is really a tier 1 or tier 2 detail. ## AI Personalization instructions: say what, how long, how it's used and what to avoid An AI Personalization field is only as good as its instructions. Every instruction should say: - **What to find**: one concrete thing, from the tier you're aiming for. - **How long**: "one sentence", "under 15 words". - **How it's used**: "written to follow 'Saw that' in a message", so it fits the copy around it. - **What to avoid**: "no generic praise such as 'innovative company'", "don't repeat the person's name", "only state what the sources show". Replace the bracketed parts of the examples below with your own offer before you save them. ## AI Personalization instructions for a buying signal A tier 1 field, such as `buying_signal`, with **Data Sources** set to **Website** and **LinkedIn Profile**: ```text Look for one specific, recent event at the company that suggests they need [what you solve]: hiring for [roles], a launch, a new market, new funding, or a post by the person about [the problem]. Write one sentence under 20 words that names the event and the problem it usually creates for [their role]. Only state what the sources show. No generic praise. Don't use the person's name. ``` Set the **Fallback value** to a tier 2 sentence that's true for every lead in the list, such as "I work with a lot of \[their role]s at \[kind of company] on \[the problem]." Leads with no signal then get a relevant line instead of a vague one. ## AI Personalization instructions for a company or role detail A tier 2 field from the company website, such as `offer_hook`, with **Data Sources** set to **Website**: ```text From the company website, find one detail that connects to [your offer]: the customers they serve, a service they sell, or the market they're in. Write one sentence under 20 words that links that detail to [the outcome you deliver]. No generic praise. Don't use the person's name. ``` A tier 2 field from the person's role, with **Data Sources** set to **LinkedIn Profile**: ```text From the person's title, headline and recent posts, write one sentence naming a likely priority for someone in their role that relates to [the problem you solve]. Phrase it as an observation, not an assumption about their problems. ``` We don't recommend an AI field for tier 3. If you write one anyway, tell it to use a personal detail only when it relates to \[the problem you solve], and to write nothing personal otherwise. ## Always set a personalization fallback Set a **Fallback value** on every field: a phrase or sentence that reads well in the same spot and is true for every lead, ideally from the tier below. Without one, a lead the research can't support gets a generic line or is held and retried later. See [Personalization fields](https://docs.versionseven.ai/help/personalization-fields). ## Use one or two personalization fields per sequence Put one or two AI fields in the highest-impact touch: the first LinkedIn message after connecting, or the first email. Never two in one message, and never in a connection note. Each field costs credits per lead and is one more chance of a held lead. ## Test personalization before activating On **Sequence**, open the step, select **Preview**, pick a few real leads and select **Generate**. Read the results as the prospect would. If several come back **Generic** or held, tighten the instruction or improve the fallback. If a signal field comes back with something that isn't really a signal, narrow what it looks for. Generating a preview spends credits, so test on a handful of leads. --- Source: https://docs.versionseven.ai/playbook/message-formatting # Message formatting How to lay out emails and LinkedIn messages in a Victoria AI sequence so they're easy to read on a phone. Prospects scan outreach on phones and in notification previews. Short lines and white space say "this will be quick"; a wall of text gets skipped. ## Format messages with short paragraphs Give each idea its own short paragraph, with a blank line between them. Write the line breaks into the step itself; they're kept when it sends. ```text Hi {first_name}, Saw that {company} is hiring its first SDRs. I help sales leaders at that stage book more first meetings by [a few words on how]. Mind if I send over [a short sample]? ``` The same message as one block reads like a template, and the ask gets lost. ## Email formatting - Plain text. No images, banners or buttons. - No bold and no bullet points in the body; they read as marketing copy. - One or two sentences per paragraph. - The ask gets its own line, at the end. - No links and no attachments. A link goes out only in an Appointment Setter reply, when the prospect asks for it. ## LinkedIn message formatting - LinkedIn keeps your line breaks, so use them: a greeting, the reason, the point and the ask, each as its own short paragraph. - No bullets or bold. - No links and no attachments. - If it looks long on a phone screen, cut it. ## Subject line formatting One short line, a few words, no sub-clauses. "quick question, `{first_name}`" works; a subject that summarises the whole email doesn't. ## The read-aloud test Read the message out loud. Every pause for breath is a line break. If you run out of breath before the end of a sentence, the sentence is too long. --- Source: https://docs.versionseven.ai/playbook/step-delays # Step delays How to space the steps of a Victoria AI sequence, from the connection acceptance window to backing off between messages. Each step's **Wait after this step** sets the gap, in days, before the next step for that lead. Our rule: start close together, then back off steeply. ## Delays between messages: start at 2 to 3 days, then back off Start with 2 to 3 days between sends, then back off steeply: 1. **First gaps**: 2 to 3 days. 2. **Next gaps**: 4 to 5 days. 3. **Later gaps**: 6 to 7 days. Early touches land while your first message is still fresh. Later ones get more room, so the sequence never feels like a chase. This holds for LinkedIn messages, emails and both together. ## Email-only cadence For an email-only sequence, send 3 to 5 emails. Our usual cadence for five: the first email, then 2 days later, 3 days later, 5 days later and 5 days later. As step waits, that's 2, 3, 5, 5 and 0 on the last email. It works best when every email gives something, and when each one stays short. See the [email-only starter](https://docs.versionseven.ai/playbook/sequences/email-only-give-first). ## Delay after a connection request: the acceptance window The wait after a **Connection Request** is the time a lead has to accept. When it ends, Victoria checks the connection once and sends the lead down **Yes** or **No**. Someone who accepts after the check stays on **No**. We recommend an acceptance window of about 10 days: long enough that most people who will accept have done so, short enough that the people who won't still hear from you by email soon after. - Too short, and people who would have accepted end up on the email-only path. - Too long, and leads who won't accept wait before getting anything. The built-in templates set the request's wait to 5 days. To follow our recommendation, raise it to about 10 days in the Sequence editor, and shorten the wait after the connection check so leads on **No** aren't kept waiting even longer. ## The wait after the connection check The branch has its own wait, between the connection check and the first step on either path. It doesn't extend the acceptance window: the check has already happened. The built-in templates set it to 5 days, and the **Sequence** tab doesn't show or change it. So in a built-in template, the first step on either path goes out the acceptance window plus the branch wait after the request. A lead who accepts during the branch wait still stays on **No**. To shorten the branch wait, ask the Copilot to change that step's delay; our starter sequences set it to 1 day. ## A wait of 0 days A wait of 0 sends the next step the same day. Use it on a last step (nothing follows it), or for a profile view right before a connection request. Avoid two messages on the same day. ## Real gaps are longer than the delay A wait counts from when the step actually went out, and steps only go out inside work hours and within daily limits. A 2-day wait set on a Friday step usually lands the next week on a Monday-to-Friday schedule. See [Work hours and daily limits](https://docs.versionseven.ai/help/work-hours-and-daily-limits). ## When to change step delays - **Senior or enterprise buyers**: lengthen gaps a little so the sequence feels measured. - **A real deadline**, such as an event: shorten gaps, but keep at least a day between messages. - **Live campaigns**: changing a wait is safe; a lead already waiting keeps its date and later leads use the new wait. See [Edit a live campaign](https://docs.versionseven.ai/help/edit-a-live-campaign). --- Source: https://docs.versionseven.ai/playbook/choosing-a-sequence-shape # Choosing a sequence shape The three sequence shapes we recommend in Victoria AI, LinkedIn only, email only and Email + LinkedIn, and which buyers and lead data each one suits. The right shape depends on where your buyers pay attention and what data your leads have. For most B2B buyers, LinkedIn comes first; for home services, email does. **Campaigns → New Campaign** offers each shape as a one-click template. See [Who to target and on which channel](https://docs.versionseven.ai/playbook/targeting-and-channels). ## LinkedIn-only sequence Our recommended shape: 1. View profile 2. Connection request, sent blank 3. Message 4. Message 5. Message The three messages go to people who accept. People who don't accept get nothing more. Use it for most B2B buyers, founders and executives, who respond more on LinkedIn than on email, and whenever you have LinkedIn URLs but not reliable emails or your mail domain isn't ready to send. The Copilot's guided setup picks LinkedIn-only when your domain's email check doesn't pass, and adds email steps once a mailbox on a passing domain is connected. Volume is bounded by acceptance and LinkedIn's per-account limits, so it suits targeted lists. Starter: [LinkedIn-only, give first](https://docs.versionseven.ai/playbook/sequences/linkedin-only-give-first). ## Email-only sequence Our recommended shape: 3 to 5 emails, usually the first, then 2 days later, 3 days later, 5 days later and 5 days later. It works well when you give, give, give: each email offers something useful. Be careful with length; a give is not a newsletter nobody opted in for. Use it for buyers who respond more on email than LinkedIn, such as home services companies (contractors, HVAC, pool and similar), when you have emails but no LinkedIn URLs, or when you need the most volume per sender. Starter: [Email-only, give first](https://docs.versionseven.ai/playbook/sequences/email-only-give-first). ## Email + LinkedIn sequence Our recommended shape: 1. View profile 2. Connection request, sent blank 3. If they accept within about 10 days: message, email, message, email, message 4. If they don't: an email branch of 3 to 5 emails Every touch after the first references the other channel, such as "sent you a note on LinkedIn too", so the two read as one conversation. Use it when your leads have both an email address and a LinkedIn URL, and you have a LinkedIn account and a mailbox to send from. It's the Copilot's default: LinkedIn warms the prospect first, and every lead gets reached, including those who never accept. Starter: [Multichannel B2B](https://docs.versionseven.ai/playbook/sequences/multichannel-b2b-default). ## Spacing in every sequence shape In all three shapes, start with 2 to 3 days between sends, then back off steeply: 2 to 3, then 4 to 5, then 6 to 7 days. See [Step delays](https://docs.versionseven.ai/playbook/step-delays) for the acceptance window and how the built-in templates set their waits. ## Match the sequence shape to your lead data Leads without an email skip email steps, and leads without a LinkedIn URL skip LinkedIn steps. The readiness check warns, or blocks above a threshold, when a meaningful share of your leads can't be reached by the sequence you chose. See [Preflight checks](https://docs.versionseven.ai/help/preflight-checks). ## When to build a sequence from scratch Start from a template unless your channel mix doesn't fit any of them. To compare two approaches, run an [A/B test](https://docs.versionseven.ai/help/ab-testing) with variations that differ in angle rather than building two campaigns. --- Source: https://docs.versionseven.ai/playbook/size-a-campaign # Size a campaign to your sending capacity How many leads to load into a Victoria AI campaign, based on your senders and daily limits, with room for an A/B test and a refill rhythm. Load a campaign with as many leads as your senders can actually reach in the next few weeks, not your whole market. A right-sized list starts sending at once, gives an A/B test enough leads to call, and leaves room to improve the copy before the list is used up. ## How many new leads a campaign can start each week Throughput is set by your senders, not by the size of the list: **Senders × per-sender daily limit × working days** The working days are the campaign's work hours: 9 AM to 5 PM, Monday to Friday, US Eastern time by default. Each sender should send for one campaign at a time, so its limits are that campaign's. ## LinkedIn throughput per account Every LinkedIn lead starts with a connection request, so connection requests set the pace: - Up to 20 connection requests a day per account, and 100 a week. - On a five-day week, the daily cap and the weekly cap meet: about 100 new leads a week per LinkedIn account. - LinkedIn messages have their own caps, so follow-up messages don't eat into new requests. A multichannel sequence that opens with a connection request runs at the same pace: about 100 new leads a week per LinkedIn account. ## Email throughput per mailbox Email limits cover every email, first touches and follow-ups alike: - Each campaign sends up to its **Emails** daily limit per mailbox, 30 by default, and each mailbox at most 500 a week. - Each lead uses one email per email step it reaches. A five-email sequence needs up to five sends per lead. - So once follow-ups are flowing, new leads per mailbox per week is roughly the mailbox's weekly sends divided by the number of email steps. At the default daily limit on a five-day week, a five-email sequence starts about 30 new leads a week per mailbox. To send more, add mailboxes rather than raising one mailbox's limits. See [Sending mailbox best practices](https://docs.versionseven.ai/help/sending-mailbox-best-practices). ## New senders start slower: the warm-up ramp For its first 14 days, a newly connected sender sends less: | Ramp day | LinkedIn requests or messages a day | Emails a day | | - | - | - | | Days 1 to 3 | 5 | 5 | | Days 4 to 7 | 10 | 10 | | Days 8 to 14 | 15 | 20 | Size the first two weeks of a new sender's campaign to these numbers, not to the full limits. See [Work hours and daily limits](https://docs.versionseven.ai/help/work-hours-and-daily-limits). ## Size the lead list from throughput Leads to load = new leads per week × the number of weeks you want the list to last. - **First batch**: two to three weeks of first touches. That's enough to reach about 100 contacted leads and judge the copy, without spending your best leads on a first draft. - **After that**: refill on a rhythm, below, rather than loading months of leads at once. Example with one LinkedIn account and three mailboxes, after the ramp: a LinkedIn-only or multichannel campaign starts about 100 new leads a week, so a first batch of two to three weeks is about 200 to 300 leads. During the free trial, a workspace holds up to 250 leads in total; see [Trial lead cap](https://docs.versionseven.ai/help/trial-lead-cap). ## Size a campaign for an A/B test An A/B test splits new leads between two variations, so each arm grows at a share of the campaign's pace. - The verdict needs 50 contacted leads per arm before it calls anything, and 100 or more per arm is better. - On a 50/50 split, that's about 200 contacted leads for a solid read: two weeks for one LinkedIn account at full pace. - On an uneven split, such as 80/20, the smaller arm sets the pace. B needs its 100 leads, so the campaign needs about five times that in total. Load enough leads for the test before you turn it on, so neither arm stalls. See [A/B testing](https://docs.versionseven.ai/help/ab-testing). ## Refill leads before the backlog runs out A campaign's backlog is its leads that haven't started the sequence. When it's empty, the campaign stops reaching new people, even though it's still active. - **Check the backlog**: **Not yet contacted** on the campaign's **Analytics** tab, or ask the Copilot "how many days of leads does this campaign have left?". It reports the backlog and the runway: about how many days the backlog lasts at the campaign's daily limits. Treat the runway as an estimate; ramps and weekly caps can slow the real pace. - **Watch for the warning**: the **Dashboard** shows "is out of leads" for an active campaign with nothing left to send. - **Refill weekly**: keep at least a week or two of backlog. Add leads on the campaign's **Leads** tab with **Find Leads**, **Add Lead** or **Upload CSV**. The weekly campaign review can also propose a top-up from the Lead database when a campaign's runway runs short, for you to approve. See [Your first month with Victoria AI](https://docs.versionseven.ai/help/first-month). --- Source: https://docs.versionseven.ai/playbook/appointment-setter-configuration # Configuring the Appointment Setter How to set up and test the Victoria AI Appointment Setter so it works every reply toward your goal and hands the right conversations to you. A well-set-up Appointment Setter answers interested prospects quickly, keeps every conversation moving toward your goal, and hands you the conversations a person should take. The settings are in **Campaigns → your campaign → Appointment Setter**. ## Give the Appointment Setter one goal and a working link Pick one **Goal** and give it a working link. **Book a meeting** with a booking page is the right goal for most outbound. The agent asks for the meeting when a prospect shows interest, shares the link, and only counts the meeting when the prospect says they booked. Don't ask it to do three jobs in **Custom instructions**, such as "answer questions, send information and book meetings". One goal, with everything else in service of it. ## The Appointment Setter always nudges toward the goal The Appointment Setter can do more than book. It answers questions from your material, shares useful assets and follows up when a prospect goes quiet. But every one of those replies should have the goal in mind and move the prospect a step toward it, even when it's answering a question or giving value. A reply that answers the question and stops leaves the prospect with nothing to do. A reply that answers it and then offers the next step keeps the conversation going: ```text Answers and stops: Yes, it works with HubSpot. Answers and nudges: Yes, it works with HubSpot, and the sync takes about a day to set up. Happy to show you what it looks like on a 15-minute call. Want the link? ``` Say how you want it to nudge in **Custom instructions**, such as "After answering a question, offer a 15-minute call to see \[what they'd get]." For how to ask, see [After a positive reply](https://docs.versionseven.ai/playbook/after-a-positive-reply). ## Match the Appointment Setter's tone to your outreach Set **Tone** to sound like the sequence: **Friendly** for casual outreach, **Direct** for brief, **Formal** for polished. A prospect who answered a casual message is put off by a stiff reply. Set **Agent name** to the name your sequence is signed with, so the conversation feels like the same person. ## Tell the Appointment Setter how to handle common objections Write your answers to the objections you hear most into **Custom instructions** or the **Knowledge base**, so the agent handles them your way: - **A hard no**: the agent closes the conversation with a short, polite acknowledgement and stops. Keep it that way; never instruct it to argue or re-pitch. - **"Not right now"**: acknowledge the timing and ask when to follow up. If they name a time, the agent waits until then, for up to 30 days. - **"I'm not the right person"**: thank them and ask who is. If they name someone, the agent hands the conversation to you so you can reach out. - **"Send me more information"**: ask what would be most useful, then share one relevant **Asset** instead of a generic deck. - **"How much does it cost?"**: pricing questions go to a person. Decide your answer ahead: a short call in most cases, or the public price or free trial for a self-serve product. [Objections and hard no's](https://docs.versionseven.ai/playbook/objections-and-hard-nos) has example replies for each. ## Hand conversations to a person on your own triggers The agent already hands off when a prospect asks for a person, raises pricing, legal or contract terms, security questionnaires or a complaint, asks something your material doesn't answer, names a better contact, or asks for a calendar invite. It also hands off after **Max agent replies per conversation**. Add your own triggers to **Always hand off when…**, in plain words, separated by commas. The agent checks every reply against them as well as the built-in ones. Good triggers are the moments a person should take: - an existing customer or partner; - a company above a size you care about, such as "more than 500 employees"; - a competitor named in the reply; - a yes to a give a person has to make, such as "the prospect says yes to the custom audit"; - a request for a proposal or a contract. A handed-off conversation shows **Needs you** in the **Inbox**, and the reason goes to **Notification email**. Set that to an inbox someone watches. See [Appointment Setter](https://docs.versionseven.ai/help/appointment-setter). ## Links and attachments in Appointment Setter replies Links belong in Appointment Setter replies, never in the sequence itself, and only when the prospect wants them: - **Your goal link**: the agent shares the booking or destination link once the prospect is interested, at most 2 times in a conversation. - **Assets**: the agent shares one when it's relevant to what the prospect asked, at most one per message and never the same one twice. Add only pages you're happy for any prospect to see. The agent shares links, never files. If a prospect needs an attachment, take over in the **Inbox** and attach it there. See [Appointment Setter knowledge and assets](https://docs.versionseven.ai/playbook/setter-knowledge-documents). ## Keep the Appointment Setter's follow-ups useful The default follow-up schedule suits most campaigns: each nudge takes a different angle and the last is a polite break-up. Shorten it for fast-moving offers, and keep **Respect work hours** on so replies don't arrive at 3 a.m. ## Test the Appointment Setter freely Select **Test it** to open **Test the Appointment Setter**. It runs the live agent against a pretend prospect with your current settings, saved or not. Nothing is sent to anyone. - Pick **LinkedIn** or **Email**, and fill in the prospect's name, company and title. - Type the prospect's replies. Each answer shows the agent's decision: a reply, a wait, a hand-off with its reason, or a close. - Select **Advance** to jump ahead while the prospect stays silent and watch the follow-ups, up to the final break-up. Play around with it. Try an interested prospect, a tough question, a price question, "not now", "we already have someone", a hard no and "remove me". Check that every reply moves toward the goal, hands off where you'd want a person, and sounds like you. Adjust the instructions and test again before you turn the switch on. The test runs the same agent as a live conversation, so it spends credits the same way and needs a balance above zero. For writing the knowledge base and assets, see [Appointment Setter knowledge and assets](https://docs.versionseven.ai/playbook/setter-knowledge-documents). --- Source: https://docs.versionseven.ai/playbook/setter-knowledge-documents # Appointment Setter knowledge and assets What to put in the Victoria AI Appointment Setter's campaign description, knowledge base and assets so its answers are accurate. The Appointment Setter answers only from what you give it. When a prospect asks something your material doesn't cover, it hands the conversation to you instead of guessing. Better material means fewer hand-offs and more accurate replies. ## The Appointment Setter's campaign description Write a short paragraph in **Campaign description**: who you're contacting, what you offer them, and the outcome you deliver. The agent uses it to answer "what is this about?". Keep it in plain language and in outcomes, not features. ## The Appointment Setter's knowledge base **Knowledge base** holds facts the agent may state when asked. One fact per line works well, for example: ```text - Works with HubSpot and Salesforce - Setup takes about two weeks - Monthly plans, cancel anytime ``` What to include: - The questions prospects actually ask, answered in a line each. Read your Inbox for them. - Real proof points: results and customer types you can back up. - Answers to "we already use X". What to leave out: - Anything you wouldn't want quoted word for word to a prospect. - Claims you can't prove, or details that are out of date. - Legal or compliance wording. Those questions should reach a person. - Long pasted pages. Short, specific lines are easier for the agent to use correctly. Update it when your offer or messaging changes. ## The Appointment Setter's assets **Assets** are links the agent may share when they help, each with a description of what it is and when it fits, such as "ROI case study for agencies". The agent shares at most one per message, never the same one twice, and only when it's relevant. Good assets: a short case study, a one-page overview, a pricing page if you publish one. Each link must be a page you're happy for any prospect to see. ## Write answers the Appointment Setter can reuse - Plain sentences, not slide fragments. - The phrasing you'd use yourself; the agent adapts it to the conversation. - No internal notes. Everything in these fields may reach a prospect in some form. --- Source: https://docs.versionseven.ai/playbook/after-a-positive-reply # After a positive reply What to send once a prospect replies with interest in Victoria AI, give first and then ask for the meeting, framed as a valuable next step. A positive reply is the start of the deal, not the end of the outreach. What you send next decides whether it turns into a meeting. ## After a positive reply: give first, then ask for the meeting When a prospect says yes to your offer or replies with interest, the order is the same every time: 1. **Give**: deliver what you offered (the intro, the sample, the audit) or answer their question properly. 2. **Ask**: then ask for the meeting. The give proves you meant it. The prospect now has something useful from you, so the meeting reads as more of the same, not as the catch. Don't skip the give and jump to "great, when are you free?". A prospect who said yes to a sample and got a calendar link instead learns that the offer was bait. ## When to ask for the meeting after a positive reply Ideally, ask once they've had time to realize the value. Deliver the give, let it land, and follow up a day or two later with the meeting ask: ```text Hey {first_name}, did the [sample / intro / audit] help? If it's useful, I can walk you through how we'd do the rest for {company} in 15 minutes. Want the link to grab a time? ``` The alternative is to ask in the same message as the give. Do this when the give is quick to take in, or when the prospect has already said they want to talk: ```text Here's the [sample] for {company}: [what it is, in a line]. The [first finding] is the one I'd look at first. If you want, I can walk you through the rest on a 15-minute call. Want the link? ``` Either way, ask once per message, and ask for one thing. ## Frame the meeting as a valuable next step, not a favour How you ask matters as much as when. A meeting framed as a donation of their time is easy to decline: ```text Weak: Would you be open to hopping on a quick call? Weak: Could I grab 15 minutes of your time this week? ``` Frame the meeting by what the prospect gets from it: ```text Better: On a 15-minute call I can show you [the three accounts in your market that are hiring right now] and how we'd reach them. Better: Happy to walk you through the rest of the audit live. It's the fastest way to see which fixes matter for {company}. ``` - Name the outcome of the call, in their terms: what they'll see, learn or leave with. - Keep it short and specific: 15 minutes, one topic. - Make it the natural continuation of the give, not a new pitch. - Don't call it a demo when you can call it what they get. ## How the Appointment Setter handles a positive reply When a campaign runs the Appointment Setter in **Autonomous** mode, it picks up the positive reply. It answers from your **Campaign description** and **Knowledge base**, can share one of your **Assets** when it's relevant, and asks for the meeting with your **Booking link** once the prospect shows interest. It asks them to reply once they've booked, because it can't see your calendar. To make it give first and frame the meeting your way, say so in **Custom instructions**: ```text When a prospect says yes to [the give], share the [asset] first. Ask for a 15-minute call to walk through [what they'll get] in the same message or the next one. Always frame the call by what they'll see on it. ``` If the give is something a person has to make, such as a custom audit, add it to **Always hand off when…**, for example "the prospect says yes to the audit". The agent then hands the conversation to you. See [Appointment Setter configuration](https://docs.versionseven.ai/playbook/appointment-setter-configuration). ## When you answer a positive reply yourself When a conversation is yours, from **Take over** in the **Inbox** or a hand-off marked **Needs you**: - Answer the same day. Interest cools fast. - Deliver the give in the reply. Here, unlike in a sequence, a link or an attachment is fine, because the prospect asked for it. - Ask for the meeting, framed as above. - When they tell you they booked, select **Mark meeting booked** so it counts in analytics. See [Inbox basics](https://docs.versionseven.ai/help/inbox-basics). --- Source: https://docs.versionseven.ai/playbook/objections-and-hard-nos # Objections and hard no's How to answer a hard no with a friendly break-up, handle common objections in Victoria AI replies, and when a pricing question should become a call. Every reply is a conversation with someone who might buy from you later, or tell a colleague about you. Handle objections where you can, and leave every "no" on good terms. ## Always answer a hard no with a friendly break-up When a prospect says a clear no, such as "not interested", "we're all set" or "no thanks", reply with a short, friendly, professional break-up and nothing more: ```text No worries, have a great day! ``` ```text No problem at all, thanks for letting me know. Have a great week. ``` Why it matters: - **It represents your brand.** The prospect remembers how you took the no. A gracious exit is the last impression you leave. - **It keeps every interaction positive.** People change jobs, budgets and priorities. A polite exit leaves the door open; an argument closes it. - **It protects your sender.** Pushing back on a no invites spam reports and blocks, which hurt your LinkedIn account and your mailbox. Never argue, re-pitch, ask why, or add a link to a hard no. Don't follow up afterwards. ## What the Appointment Setter does with a hard no On a clear no, the Appointment Setter closes the conversation as declined, with at most a short acknowledgement, and stops following up. A decline doesn't add the person to Do Not Contact. When a prospect asks not to be contacted again, such as "remove me" or "stop messaging me", it closes the conversation as an opt-out and adds them to your organization's Do Not Contact list, for every campaign and channel. See [Replies and sentiment](https://docs.versionseven.ai/help/replies-and-sentiment). ## Handle specific objections, then return to the goal Not every pushback is a no. "Not right now", "we already have someone" and "send me info" are objections you can often answer. The pattern: 1. Acknowledge it in a few words. Never argue. 2. Answer it in a line or two, with something real: a fact, a difference, a give. 3. Bring it back to the goal with one small ask. Write your answers to the objections you hear most into the Appointment Setter's **Knowledge base** or **Custom instructions**, so it handles them your way. See [Appointment Setter configuration](https://docs.versionseven.ai/playbook/appointment-setter-configuration). ## Objection: "Not interested" "Not interested" is a hard no. Answer with the break-up, and stop: ```text No worries, have a great day! ``` If they add a reason you can fix, such as "not interested, we only work with agencies", you can answer that reason once in a line. If not, let it go. ## Objection: "Send me more info" Sometimes this is a polite brush-off, sometimes a real request. Treat it as real, and make it specific: ask what would help, or send one relevant thing instead of a deck. ```text Happy to. What's most useful: [how it works for teams like yours] or [what it costs to get started]? ``` ```text Here's a one-page overview of how we'd do [the outcome] for [companies like theirs]: [link] If it looks relevant, I can walk you through how it'd apply to {company} in 15 minutes. ``` A link is fine here because they asked for it. Add the page to the Appointment Setter's **Assets** so it can share it. ## Objection: "We already have someone for this" Respect it, show one real difference or how you'd work alongside them, and leave a give on the table: ```text Makes sense. [One real difference, such as: we only handle the part they usually don't, X.] Happy to send [the give] so you can compare. No pressure either way. ``` Only claim a difference you can back up. If you have no real difference for their situation, treat it as a no and break up politely. ## Objection: "Bad timing" or "Not right now" Acknowledge the timing and ask when to come back: ```text Totally fair. When would be a better time to pick this up, next month or next quarter? ``` If they name a time, the Appointment Setter waits until then before the next touch, for up to 30 days. For anything later, such as "next quarter", take over in the **Inbox** and set yourself a reminder. If they say "never", it's a hard no: break up politely. ## Objection: "How much does it cost?" In most cases, nudge toward a short call before giving a price. A price without context gets compared to nothing, and the call gives you or your rep the chance to help them see the value and what fits first: ```text It depends on [what drives the price, such as team size or volume]. On a 15-minute call I can show you what it'd do for {company} and give you an exact number. Want the link? ``` The exception is a self-serve or SaaS product with a public price or a free trial. There, a call is usually unnecessary friction. Give the price or the trial plainly: ```text Plans start at [your public price] a month, and there's a free trial: [link]. Happy to answer anything while you try it. ``` The Appointment Setter never makes up a price. It hands pricing questions to you, marked **Needs you** in the **Inbox**, so this answer is yours to write. ## Objection: "I'm not the right person" Thank them and ask who is: ```text Thanks for letting me know. Who would be the best person to talk to about [the problem]? ``` When they name someone, the Appointment Setter hands the conversation to you so you can reach out to that person. See [Who to target and on which channel](https://docs.versionseven.ai/playbook/targeting-and-channels) for contacting several people at one company. --- Source: https://docs.versionseven.ai/playbook/benchmarks # Outbound benchmarks Typical cold outreach rates and the healthier targets to aim for with Victoria AI, mapped to its metrics, and how much data you need before judging. Use these ranges to tell whether a campaign is healthy and, if it isn't, which stage to fix first. They're guidance, not guarantees: your market, offer, list and sender all move them. ## Typical cold outreach rates and Victoria AI targets "Typical" is the range most cold outreach lands in. "Target with Victoria" is a healthy range for a campaign that follows this playbook: blank connection requests from an optimized profile, give-first copy, personalization that's relevant to the offer, and the Appointment Setter answering every reply. | Stage | Typical | Target with Victoria | | - | - | - | | LinkedIn connection acceptance | 20% to 35% | 30% to 50% | | LinkedIn replies, of accepted connections | 5% to 15% | 10% to 25% | | Cold email replies, of contacted | 1% to 5% | 3% to 8% | | Positive replies, of contacted | 1% to 3% | 2% to 5% | | Meetings, of positive replies | 30% to 50% | 40% to 60% | | Meetings per 100 leads contacted | 0.5 to 2 | 1 to 3 | A campaign inside the typical range isn't failing, and one below it isn't hopeless. The gap between the two columns is where the playbook earns its keep. ## Where each benchmark lives in Victoria AI analytics Read these on **Campaigns → your campaign → Analytics**, or across campaigns on the **Dashboard**, with the range set to **Lifetime** or **All** while a campaign is young. - **LinkedIn connection acceptance**: **Accept rate**. Over a short date range it can read high, because acceptances in the range can come from requests sent before it. - **LinkedIn replies, of accepted connections**: Victoria doesn't show this rate directly. On a LinkedIn-only campaign, divide **Replied** by **Connected**. - **Cold email replies, of contacted**: **Reply rate** on an email-only campaign. On a multichannel campaign, **Reply rate** blends both channels. - **Positive replies, of contacted**: **Positive reply rate**. - **Meetings, of positive replies**: **Meeting rate**. - **Meetings per 100 leads contacted**: **Meetings booked** divided by **Contacted**, times 100. Every rate counts leads, not messages, and **Meetings booked** counts confirmed bookings that aren't checked against a calendar. See [Analytics metrics](https://docs.versionseven.ai/help/analytics-metrics). ## Signals that aren't benchmarks - **Open rate** is directional only. Privacy-protected inboxes hide opens and some mail clients open every image automatically, so don't judge a campaign by it. - **Bounces** should stay rare. Aim for well under 2% of emails sent. A mailbox where more than 8% of recent emails go to unreachable addresses has its sequence emails paused until the rate drops. See [Sender limits and ramp](https://docs.versionseven.ai/help/sender-limits-and-ramp). - **Negative replies** aren't a failure in themselves. A clear no, answered with a polite break-up, is a finished conversation. A high share of "not relevant" or "wrong person" replies points at targeting. ## How much data you need before judging a campaign Wait for at least about 100 contacted leads before comparing a campaign with these ranges. Below that, one reply moves the rate by a point or more. Give leads time to get through the sequence, too. A lead contacted yesterday has had one touch, and many replies come on the second or third. Judge on leads contacted at least two to three weeks ago, or check how many are **Completed**. ## How much data an A/B test needs The A/B verdict says **Not enough data yet** until each arm has 50 contacted leads. That's the floor for a verdict, not a target: aim for 100 or more contacted per arm before acting on anything but a large difference. At positive reply rates of a few percent, a small arm is easily swung by one or two replies. See [A/B testing](https://docs.versionseven.ai/help/ab-testing). ## When a campaign is below the benchmarks Find the first stage that's below its range and fix that one before anything after it. A low acceptance rate starves every later stage, so better copy can't help until more people accept. [Diagnose a campaign by its bottleneck](https://docs.versionseven.ai/playbook/diagnose-a-campaign) is the step-by-step checklist. --- Source: https://docs.versionseven.ai/playbook/diagnose-a-campaign # Diagnose a campaign by its bottleneck A step-by-step checklist for finding why a Victoria AI campaign underperforms, starting at the first weak stage of the funnel and fixing that first. A campaign is a funnel: contacted, accepted, replied, positive, booked. Each stage feeds the next, so the first weak stage limits everything after it. Always start with the bottleneck, and fix it before touching anything downstream. ## The campaign diagnosis checklist Work through these in order and stop at the first one that fails. The metric names are the ones on **Campaigns → your campaign → Analytics**. 1. **Is it sending?** **Contacted** should be growing. If not, the cause is operational, not copy. See "Rule out operational causes first" below. 2. **Is there enough data?** Wait for about 100 contacted leads, contacted at least two to three weeks ago, before judging rates. See [Outbound benchmarks](https://docs.versionseven.ai/playbook/benchmarks). 3. **Find the bottleneck.** Compare each stage with the benchmarks, in funnel order: **Accept rate**, then replies, then **Positive reply rate**, then **Meeting rate**. The first stage below its range is the bottleneck. 4. **Low acceptance on LinkedIn?** Fix the sender's LinkedIn profile, and send requests blank. 5. **Accepted but few replies?** Fix message relevance: personalize to a real problem, find a way to give, and tighten the opener. 6. **Email gets few replies?** Fix deliverability first (dedicated domain, warm-up, DNS), then the copy. 7. **Replies but few positive?** Fix the offer and the targeting. 8. **Positive replies but few meetings?** Make the meeting more valuable, frame it as a valuable next step, and give first. 9. **Change one thing at a time**, ideally as an [A/B test](https://docs.versionseven.ai/help/ab-testing), and re-measure once each arm has enough contacted leads. Each bottleneck below has its own checks and fixes. ## Rule out operational causes first Before judging copy, make sure the campaign is actually running as intended. A campaign that sends little looks like one that gets no replies. - A red **This campaign isn't sending** banner, a paused switch, or a disconnected sender. - **Not yet contacted** at zero: the campaign has run out of leads. The **Dashboard** shows "is out of leads" for an active campaign with nothing left to send. - Daily limits, sender caps or a sender still in its warm-up ramp holding volume down. - A credit balance at zero or a failed payment. - AI Personalization fields holding leads because they have no working fallback. See [Leads aren't moving](https://docs.versionseven.ai/help/troubleshooting-leads-not-moving) for every cause and its fix. You can also ask the Copilot "why isn't this campaign sending?". ## Bottleneck: low LinkedIn connection acceptance Signs: **Accept rate** below the benchmark range, so few leads reach the **Yes** path where LinkedIn messages go out. Check, in order: 1. **The sender's profile.** The request shows only a photo, a name and a headline. Is the headshot clear, professional and recent? Does the headline say in a few words who the sender helps? See [Optimize your LinkedIn profile](https://docs.versionseven.ai/playbook/linkedin-profile). 2. **The request itself.** Send it blank. A note that pitches gets ignored or declined. See [LinkedIn connection requests](https://docs.versionseven.ai/playbook/linkedin-connection-requests). 3. **The acceptance window.** If the wait after the request is short, people who would have accepted end up on **No**. About 10 days works well. See [Step delays](https://docs.versionseven.ai/playbook/step-delays). 4. **The targeting.** Very senior or very large-company leads accept less. If the profile and request are right, check that the list matches who you help. ## Bottleneck: accepted connections but few replies Signs: a healthy **Accept rate** but few people reply on LinkedIn (**Replied** divided by **Connected**, on a LinkedIn-only campaign). Check, in order: 1. **Relevance.** Does the first message name a real problem this person has? Move up the personalization hierarchy: a buying signal beats a company or role detail, and both beat a personal detail. See [Personalization that works](https://docs.versionseven.ai/playbook/personalization-instructions). 2. **The give.** Does every message offer something useful, or does it ask? Turn asks into gives. See [Give before you ask](https://docs.versionseven.ai/playbook/give-before-you-ask). 3. **The opener.** Use "I help X do Y by Z" or a specific, true observation followed by one question. See [Openers that get replies](https://docs.versionseven.ai/playbook/openers). 4. **Length and slop.** Two or three short lines, no em dashes, nothing that reads like a template. See [Slop](https://docs.versionseven.ai/playbook/slop-language). ## Bottleneck: cold email gets few replies Signs: plenty of **Emails sent** but a **Reply rate** below the email benchmark, especially when LinkedIn on the same list does better. Check deliverability first, because no copy works in the spam folder: 1. **Sending domain and mailbox.** Is the campaign sending from a dedicated sending domain, not the mailbox the team uses every day? 2. **Warm-up.** Was the mailbox warmed up with a warm-up service before sending? Victoria ramps volume but doesn't warm mailboxes. 3. **DNS.** Does the mailbox's chip on **Sender Accounts** read **DNS ok**? Fix SPF, DKIM and DMARC first. See [DNS setup](https://docs.versionseven.ai/help/dns-setup). 4. **Bounces and links.** Use verified emails, and keep links and attachments out of sequence emails. Then check the copy the same way as for LinkedIn: relevance, the give, the opener and length. See [Sending mailbox best practices](https://docs.versionseven.ai/help/sending-mailbox-best-practices) and [Cold email principles](https://docs.versionseven.ai/playbook/cold-email-principles). ## Bottleneck: replies but few positive replies Signs: a healthy reply rate, but a low **Positive reply rate**: replies are mostly negative or neutral. Check, in order: 1. **Read the replies.** In the **Inbox**, filter by the campaign and read the last 20. "Not relevant" and "wrong person" point at targeting; "not interested" across the board points at the offer. 2. **The targeting.** Tighten titles, seniority, company size and industry so the list is people who own the problem. See [Who to target and on which channel](https://docs.versionseven.ai/playbook/targeting-and-channels). 3. **The offer.** Is the give something this buyer wants? Is the "I help X do Y by Z" line specific enough that they recognise themselves? See [Framing your value proposition](https://docs.versionseven.ai/playbook/value-proposition). ## Bottleneck: positive replies but no meetings booked Signs: **Positive replies** coming in, but a low **Meeting rate** (**Meetings booked** divided by **Positive replies**). Check, in order: 1. **The Appointment Setter is on**, in **Autonomous** mode, with a working **Booking link**, and conversations marked **Needs you** are answered the same day. 2. **Give first.** Does the reply deliver what was offered before asking for the meeting? See [After a positive reply](https://docs.versionseven.ai/playbook/after-a-positive-reply). 3. **Make the meeting more valuable.** Is it framed as a valuable next step, with what they'll see or get on the call, rather than a donation of their time? 4. **Unanswered questions.** Read the conversations for questions the agent couldn't answer, and add those facts to its **Knowledge base**. 5. **Bookings not recorded.** **Meetings booked** counts only bookings the prospect confirmed or you marked. If prospects book without saying so, select **Mark meeting booked** in the **Inbox**. ## After the fix: measure the change Change one thing, at the bottleneck, and give it enough leads to show a result. An A/B test makes the comparison clean: Variation B differs from A at the bottleneck, such as a new opener or a new give, and the verdict needs 50 contacted leads per arm before it calls anything. Then start the checklist again: fixing one stage often reveals the next. The weekly campaign review follows the same order and proposes changes for you to approve in the Copilot. See [Your first month with Victoria AI](https://docs.versionseven.ai/help/first-month). --- Source: https://docs.versionseven.ai/playbook/sequences/multichannel-b2b-default # Starter sequence: multichannel B2B Our recommended Email + LinkedIn sequence in Victoria AI, a blank request with a 10-day window, then two branches that reference each other. This is a starting point, not a finished campaign. Replace the bracketed offer and the sign-off with your own, and keep only claims you can back up. It follows our recommended Email + LinkedIn shape: a profile view and a blank connection request, then message, email, message, email, message for people who accept within about 10 days, and four emails for people who don't. Every touch after the first ties back to the other channel. It uses one AI Personalization field, `{recent_news}`, in the first email on the **No** path. Create it before activating (see "Create the AI Personalization field" below), or rewrite that line. ## The multichannel starter sequence ```json sequence { "variation_a": [ { "id": 1, "type": "view_profile", "delay": 1 }, { "id": 2, "type": "linkedin_connection", "message": "", "delay": 10 }, { "id": 3, "type": "conditional", "conditionalType": "linkedin_connected", "isConditional": true, "delay": 1, "branches": { "yes": [ { "id": 4, "type": "linkedin_message", "message": "Hey {first_name}, I help [specific kind of company] [do one specific, niche thing] by [a few words on how].\n\nMind if I send over [the give: a short teardown / a sample] for {company}?", "delay": 2 }, { "id": 5, "type": "email", "subject": "from LinkedIn", "content": "Hi {first_name},\n\nI sent you a note on LinkedIn too, so I'll keep this short.\n\nI offered to send [the give]. It takes [a few minutes] to read, and it's yours whether or not we ever talk.\n\nWant it?\n\nJordan", "delay": 3 }, { "id": 6, "type": "linkedin_message", "message": "{first_name}, I sent you an email as well, so here's the short version.\n\nOne thing that's working for [teams like yours] right now: [one concrete idea, in a line].\n\nHappy to send [the give] if it's useful.", "delay": 5 }, { "id": 7, "type": "email", "subject": "an idea for {company}", "content": "Hi {first_name},\n\nA second idea to go with my LinkedIn message: [another idea or a resource, in two lines].\n\nIf it's useful, I can [make a quick intro / share how teams like yours set it up].\n\nJordan", "delay": 7 }, { "id": 8, "type": "linkedin_message", "message": "Last note from me, {first_name}, here and by email. If the timing's off, no problem at all. The offer of [the give] stands whenever it's useful.", "delay": 0 } ], "no": [ { "id": 9, "type": "email", "subject": "question about {company}", "content": "Hi {first_name},\n\nNoticed {recent_news}.\n\nIs [the thing your offer helps with] on your list this quarter?\n\nI sent you a connection request on LinkedIn too, in case that's easier.\n\nJordan", "delay": 2 }, { "id": 10, "type": "email", "subject": "[the give] for {company}", "content": "Hi {first_name},\n\nI'd still like to connect on LinkedIn, but email works too.\n\nI help [specific kind of company] [do one specific, niche thing] by [a few words on how]. Mind if I send over [the give]?\n\nJordan", "delay": 3 }, { "id": 11, "type": "email", "subject": "an idea for {company}", "content": "Hi {first_name},\n\nNo need to reply to this one. One thing that's working for [teams like yours]: [one concrete idea, in a line or two].\n\nIf LinkedIn is easier for you, my connection request is there.\n\nJordan", "delay": 5 }, { "id": 12, "type": "email", "subject": "last note", "content": "Hi {first_name},\n\nI'll leave it here so I'm not crowding your inbox or your LinkedIn.\n\nIf [the problem] comes up later, just reply. The offer of [the give] still stands.\n\nJordan", "delay": 0 } ] } } ], "variation_b": [] } ``` Replace "Jordan" with the sender's name, and every bracketed phrase with your own words. ## Create the AI Personalization field In the Sequence editor, open step 9 (the first email on the **No** path), select **Insert Variable → AI Personalization → +**, and create: - **Field Name**: `recent_news` - **AI Instructions**: "Name one specific recent launch, announcement or milestone from the company's website, as a short phrase that can follow 'Noticed' in a message. No generic praise." - **Fallback value**: "the growth at your company" (or any phrase that reads well after "Noticed") - **Data Sources**: **Website** See [Personalization that works](https://docs.versionseven.ai/playbook/personalization-instructions). ## Why it works - **Step 1, View Profile**: a light first touch. The prospect may see your name before the request arrives. - **Step 2, blank Connection Request**: in our testing, requests with no note were accepted 10% to 30% more often, because a blank request carries no angle or ask. Acceptance is what unlocks LinkedIn messages. - **The timing of the acceptance window**: Victoria checks the connection once, when the wait after the request ends, and the branch's own wait only spaces the check from the first step on either path. So the 10-day wait sits on step 2, and the branch waits 1 day. A lead has about 10 days to accept, and the first message or email goes out the day after the check. The built-in template instead waits 5 days on the request and 5 days on the branch: its first branch step lands at a similar time, but anyone who accepts after the check goes down **No**. - **Step 4, first LinkedIn message**: the "I help X do Y by Z" opener with a give-first CTA. See [Openers that get replies](https://docs.versionseven.ai/playbook/openers). - **Steps 5 to 8, Yes path**: message, email, message, email, message. Each one mentions the other channel ("I sent you a note on LinkedIn too", "I sent you an email as well"), so it reads as one conversation, not two strangers writing. Every touch gives something: the offer, an idea, a second idea, the offer left open. - **Steps 9 to 12, No path**: four emails for people who didn't accept, so every lead is reached. The first uses the question opener with the AI field, and each one mentions the LinkedIn request so the two channels still tie together. - **Delays**: Yes path 2, 3, 5 and 7 days; No path 2, 3 and 5 days. Start close together and back off steeply. The last step on each path waits 0 because nothing follows it. - **Throughout**: no links, no unprovable claims, no slop, short first-person sentences and one small ask per message. When someone replies, the sequence stops and the Appointment Setter can take over: deliver the give, follow up and work the deal. To test a second angle, turn on [A/B testing](https://docs.versionseven.ai/help/ab-testing) and change the opener and the give in Variation B, not just the wording. --- Source: https://docs.versionseven.ai/playbook/sequences/linkedin-only-give-first # Starter sequence: LinkedIn-only, give first Our recommended LinkedIn-only sequence in Victoria AI, a blank connection request then three give-first messages, with full copy and timing. This is a starting point, not a finished campaign. Replace the bracketed parts with your own words and keep only claims you can back up. It follows our recommended LinkedIn-only shape: view the profile, send a blank connection request, then three short messages to people who accept. It needs only a LinkedIn account and uses no AI Personalization fields, so it costs no credits to personalize. ## The LinkedIn-only starter sequence ```json sequence { "variation_a": [ { "id": 1, "type": "view_profile", "delay": 1 }, { "id": 2, "type": "linkedin_connection", "message": "", "delay": 10 }, { "id": 3, "type": "conditional", "conditionalType": "linkedin_connected", "isConditional": true, "delay": 1, "branches": { "yes": [ { "id": 4, "type": "linkedin_message", "message": "Hey {first_name}, I help [specific kind of company] [do one specific, niche thing] by [a few words on how].\n\nMind if I [make a quick intro / send over a short sample] for {company}?", "delay": 3 }, { "id": 5, "type": "linkedin_message", "message": "{first_name}, one thing that's working for [companies like yours] right now: [one concrete idea, in a line].\n\nHappy to send [the give] if it's useful. No call needed.", "delay": 5 }, { "id": 6, "type": "linkedin_message", "message": "Last note from me, {first_name}.\n\nIf the timing's off, no problem at all. The offer of [the give] stands whenever it's useful.", "delay": 0 } ], "no": [] } } ], "variation_b": [] } ``` ## Why it works - **Step 1, View Profile**: a light first touch. The prospect may see your name before the request arrives. The 1-day wait puts the request the next day. - **Step 2, blank Connection Request**: in our testing, requests with no note were accepted 10% to 30% more often, because a blank request carries no angle or ask. Your photo and headline do the introducing; see [Optimize your LinkedIn profile](https://docs.versionseven.ai/playbook/linkedin-profile). - **The 10-day wait on step 2 is the acceptance window.** Victoria checks the connection once when it ends. We use about 10 days instead of the built-in template's 5 days, because nothing is waiting on the **No** path, so a longer window costs nothing. - **Step 3, the branch**: the **No** path is empty, so people who don't accept get nothing further. Victoria checks their connection up to 3 more times, 24 hours apart, before marking them complete, in case they accept late. The branch's 1-day wait sends the first message the day after the check, while the connection is fresh. - **Step 4, first message**: the "I help X do Y by Z" opener, then a give-first CTA. Saying yes costs the prospect nothing. See [Openers that get replies](https://docs.versionseven.ai/playbook/openers). - **Step 5, second message**: gives an idea outright and offers the give again, with no meeting ask. - **Step 6, last message**: a short close that leaves the offer open. - **Delays**: 3 days, then 5. Start close together and back off steeply, so the sequence never feels like a chase. The last step's wait is 0 because nothing follows it. - **Throughout**: three short messages, first person, no links, no slop and nothing claimed that can't be shown. When someone replies, the sequence stops and the Appointment Setter can take over: deliver the give, follow up and work the deal. LinkedIn-only volume is bounded by acceptance and per-account LinkedIn limits, so this shape works best on a focused list. To reach people who don't accept, switch to the [multichannel starter](https://docs.versionseven.ai/playbook/sequences/multichannel-b2b-default) once a mailbox is connected. --- Source: https://docs.versionseven.ai/playbook/sequences/email-only-give-first # Starter sequence: email-only, give first Our recommended five-email sequence in Victoria AI, spaced 2, 3, 5 and 5 days apart, where every email gives something, with full copy. This is a starting point, not a finished campaign. Replace the bracketed parts and the sign-off with your own, and keep only claims you can back up. It follows our recommended email-only shape: five short emails that give, give, give. It uses one AI Personalization field, `{company_highlight}`, in email 2. Create it before activating (see "Create the AI Personalization field" below), or rewrite that line. ## The email-only starter sequence ```json sequence { "variation_a": [ { "id": 1, "type": "email", "subject": "question about {company}", "content": "Hi {first_name},\n\nI help [home services companies like yours] [fill their schedule in the slow months] by [a few words on how].\n\nMind if I send over [the give: a short sample / a quick audit of your listing]?\n\nJordan", "delay": 2 }, { "id": 2, "type": "email", "subject": "{first_name}, quick one", "content": "Hi {first_name},\n\nSaw {company_highlight}.\n\nAre you taking on more [jobs like that] this month?\n\nIf so, [the give] might help. Happy to send it over.\n\nJordan", "delay": 3 }, { "id": 3, "type": "email", "subject": "an idea for {company}", "content": "Hi {first_name},\n\nOne thing that's working for [companies like yours] right now: [one concrete idea, in a line or two].\n\nNo need to reply. I thought it might be useful.\n\nJordan", "delay": 5 }, { "id": 4, "type": "email", "subject": "[the give] for {company}", "content": "Hi {first_name},\n\nI put together [a one-page checklist / a short sample] on [the problem you solve], for [companies like yours].\n\nWant me to send it? It takes [a few minutes] to read.\n\nJordan", "delay": 5 }, { "id": 5, "type": "email", "subject": "last note", "content": "Hi {first_name},\n\nI'll leave it here so I'm not crowding your inbox.\n\nIf [the problem] comes up later, just reply. The offer of [the give] still stands.\n\nJordan", "delay": 0 } ], "variation_b": [] } ``` Replace "Jordan" with the sender's name. ## Create the AI Personalization field In the Sequence editor, open email 2, select **Insert Variable → AI Personalization → +**, and create: - **Field Name**: `company_highlight` - **AI Instructions**: "From the company's website, name one specific service, service area or recent project, written as a short phrase that can follow 'Saw' in a message, such as 'that you've added pool resurfacing'. Under 15 words. No generic praise." - **Fallback value**: "your website and the work your team does" - **Data Sources**: **Website** See [Personalization that works](https://docs.versionseven.ai/playbook/personalization-instructions). ## Why it works - **Email 1**: the "I help X do Y by Z" opener, then a give-first CTA. No link, no meeting ask. See [Openers that get replies](https://docs.versionseven.ai/playbook/openers). - **Email 2**: the question opener: one specific, true observation from the AI field, then one question about it, and the give offered again. - **Email 3**: gives an idea outright and asks for nothing. - **Email 4**: offers a concrete resource, in two lines, rather than pasting it in. Giving is not a newsletter nobody opted into. - **Email 5**: a short close that leaves the offer open and makes "not now" an easy answer. - **Delays**: 2, 3, 5 and 5 days, our usual email-only cadence: close together while the first email is fresh, then more room so the sequence never crowds the inbox. The last step's wait is 0 because nothing follows it. - **Personalization**: `{first_name}` and `{company}`, plus the one AI field in email 2. A lead with no first name gets "Hi there,", and one with no company name gets "your company", so the copy reads correctly either way. - **Who it suits**: buyers who answer email more than LinkedIn, such as home services companies. See [Who to target and on which channel](https://docs.versionseven.ai/playbook/targeting-and-channels). When someone replies, the sequence stops and the Appointment Setter can take over: deliver the give, follow up and work the deal. Before activating, check that the sending domain passes its SPF, DKIM and DMARC check; see [DNS setup](https://docs.versionseven.ai/help/dns-setup). --- Source: https://docs.versionseven.ai/api-reference # API Reference Every endpoint of the Victoria AI REST API, generated from its OpenAPI specification. Requests and responses are JSON, and every request carries your API key as a Bearer token. Base URL: `https://api.versionseven.ai/v1` ## Auth Check that an API key works and see which organization it belongs to. - [Verify an API key](https://docs.versionseven.ai/api-reference/auth/verify-api-key): `GET /v1/auth/verify` ## Sender accounts The LinkedIn and email accounts connected to your organization for outreach, whether each one is still connected, and links that connect or reconnect one. - [List sender accounts](https://docs.versionseven.ai/api-reference/accounts/list-accounts): `GET /v1/accounts` - [Create a connect link](https://docs.versionseven.ai/api-reference/accounts/create-connect-link): `POST /v1/accounts/connect-link` ## Leads Create leads, enrol them in campaigns, find them with filters, update them, and remove them from campaigns. - [Create a lead](https://docs.versionseven.ai/api-reference/leads/create-lead): `POST /v1/leads` - [List leads](https://docs.versionseven.ai/api-reference/leads/list-leads): `GET /v1/leads` - [Retrieve a lead](https://docs.versionseven.ai/api-reference/leads/retrieve-lead): `GET /v1/leads/{lead_id}` - [Update a lead](https://docs.versionseven.ai/api-reference/leads/update-lead): `PATCH /v1/leads/{lead_id}` - [Delete a lead or remove it from a campaign](https://docs.versionseven.ai/api-reference/leads/remove-lead-from-campaign): `DELETE /v1/leads/{lead_id}` - [Create a standalone lead](https://docs.versionseven.ai/api-reference/leads/create-standalone-lead): `POST /v1/leads/create` (deprecated) ## Campaigns Create and manage campaigns: their sequences, AI fields, senders, reply agent, activation preflight, analytics, A/B tests, bookings, lead queue, and the webhooks that receive prospect replies. - [List campaigns](https://docs.versionseven.ai/api-reference/campaigns/list-campaigns): `GET /v1/campaigns` - [Create a campaign](https://docs.versionseven.ai/api-reference/campaigns/create-campaign): `POST /v1/campaigns` - [Retrieve a campaign](https://docs.versionseven.ai/api-reference/campaigns/retrieve-campaign): `GET /v1/campaigns/{campaign_id}` - [Update a campaign](https://docs.versionseven.ai/api-reference/campaigns/update-campaign): `PATCH /v1/campaigns/{campaign_id}` - [Replace a campaign's sequence](https://docs.versionseven.ai/api-reference/campaigns/replace-sequence): `PUT /v1/campaigns/{campaign_id}/sequence` - [Update a sequence step](https://docs.versionseven.ai/api-reference/campaigns/update-sequence-step): `PATCH /v1/campaigns/{campaign_id}/sequence/steps/{step_id}` - [Get campaign analytics](https://docs.versionseven.ai/api-reference/campaigns/get-campaign-analysis): `GET /v1/campaigns/{campaign_id}/analysis` - [Get a campaign's queue](https://docs.versionseven.ai/api-reference/campaigns/get-campaign-queue): `GET /v1/campaigns/{campaign_id}/queue` - [Get the activation preflight](https://docs.versionseven.ai/api-reference/campaigns/get-campaign-preflight): `GET /v1/campaigns/{campaign_id}/preflight` - [Assign sender accounts](https://docs.versionseven.ai/api-reference/campaigns/assign-senders): `PUT /v1/campaigns/{campaign_id}/senders` - [List AI fields](https://docs.versionseven.ai/api-reference/campaigns/list-ai-fields): `GET /v1/campaigns/{campaign_id}/ai-fields` - [Create an AI field](https://docs.versionseven.ai/api-reference/campaigns/create-ai-field): `POST /v1/campaigns/{campaign_id}/ai-fields` - [Update an AI field](https://docs.versionseven.ai/api-reference/campaigns/update-ai-field): `PATCH /v1/campaigns/{campaign_id}/ai-fields/{field_id}` - [Get the reply agent](https://docs.versionseven.ai/api-reference/campaigns/get-responder): `GET /v1/campaigns/{campaign_id}/responder` - [Update the reply agent](https://docs.versionseven.ai/api-reference/campaigns/update-responder): `PATCH /v1/campaigns/{campaign_id}/responder` - [Preview a campaign's messages](https://docs.versionseven.ai/api-reference/campaigns/preview-campaign): `POST /v1/campaigns/{campaign_id}/preview` - [List meetings booked](https://docs.versionseven.ai/api-reference/campaigns/list-bookings): `GET /v1/campaigns/{campaign_id}/bookings` - [Turn A/B testing on or off](https://docs.versionseven.ai/api-reference/campaigns/set-ab-testing): `POST /v1/campaigns/{campaign_id}/ab-testing` - [Promote an A/B winner](https://docs.versionseven.ai/api-reference/campaigns/promote-ab-winner): `POST /v1/campaigns/{campaign_id}/ab-testing/promote` - [Get daily stats](https://docs.versionseven.ai/api-reference/campaigns/get-daily-stats): `GET /v1/campaigns/{campaign_id}/daily-stats` - [Get the step funnel](https://docs.versionseven.ai/api-reference/campaigns/get-step-funnel): `GET /v1/campaigns/{campaign_id}/step-funnel` - [Get the sender breakdown](https://docs.versionseven.ai/api-reference/campaigns/get-sender-breakdown): `GET /v1/campaigns/{campaign_id}/senders/breakdown` - [Get personalization quality](https://docs.versionseven.ai/api-reference/campaigns/get-personalization-quality): `GET /v1/campaigns/{campaign_id}/personalization-quality` - [Compare A/B cohorts](https://docs.versionseven.ai/api-reference/campaigns/get-ab-cohorts): `GET /v1/campaigns/{campaign_id}/ab-cohorts` - [List a campaign's webhooks](https://docs.versionseven.ai/api-reference/campaigns/list-webhooks): `GET /v1/campaigns/{campaign_id}/webhooks` - [Create a webhook](https://docs.versionseven.ai/api-reference/campaigns/create-webhook): `POST /v1/campaigns/{campaign_id}/webhooks` - [Delete a webhook](https://docs.versionseven.ai/api-reference/campaigns/delete-webhook): `DELETE /v1/campaigns/{campaign_id}/webhooks/{webhook_id}` - [Set or rotate a webhook secret](https://docs.versionseven.ai/api-reference/campaigns/rotate-webhook-secret): `POST /v1/campaigns/{campaign_id}/webhooks/{webhook_id}/secret` - [Activate a webhook](https://docs.versionseven.ai/api-reference/campaigns/activate-webhook): `POST /v1/campaigns/{campaign_id}/webhook/activate` (deprecated) - [Deactivate a webhook](https://docs.versionseven.ai/api-reference/campaigns/deactivate-webhook): `POST /v1/campaigns/{campaign_id}/webhook/deactivate` (deprecated) - [List sequence templates](https://docs.versionseven.ai/api-reference/campaigns/list-sequence-templates): `GET /v1/campaigns/sequence-templates` (deprecated) - [List webhook examples](https://docs.versionseven.ai/api-reference/campaigns/list-webhook-examples): `GET /v1/campaigns/webhook/examples` (deprecated) ## Pipelines Read and update the CRM pipelines that deals move through, along with their stages. - [List pipelines](https://docs.versionseven.ai/api-reference/crm-pipelines/list-pipelines): `GET /v1/crm/pipelines` - [Retrieve a pipeline](https://docs.versionseven.ai/api-reference/crm-pipelines/retrieve-pipeline): `GET /v1/crm/pipelines/{pipeline_id}` - [Update a pipeline](https://docs.versionseven.ai/api-reference/crm-pipelines/update-pipeline): `PATCH /v1/crm/pipelines/{pipeline_id}` ## Deals Create, list, retrieve and update deals in the Victoria AI CRM. - [List deals](https://docs.versionseven.ai/api-reference/crm-deals/list-deals): `GET /v1/crm/deals` - [Create a deal](https://docs.versionseven.ai/api-reference/crm-deals/create-deal): `POST /v1/crm/deals` - [Retrieve a deal](https://docs.versionseven.ai/api-reference/crm-deals/retrieve-deal): `GET /v1/crm/deals/{deal_id}` - [Update a deal](https://docs.versionseven.ai/api-reference/crm-deals/update-deal): `PATCH /v1/crm/deals/{deal_id}` ## Reference Blank sequence templates to start from, and example payloads for building a webhook receiver. - [List sequence templates](https://docs.versionseven.ai/api-reference/reference/list-sequence-templates): `GET /v1/sequence-templates` - [List webhook examples](https://docs.versionseven.ai/api-reference/reference/list-webhook-examples): `GET /v1/webhooks/examples` ## Webhook events - [prospect_response](https://docs.versionseven.ai/api-reference/webhooks/prospect-response): Sent to a campaign's webhooks when a prospect replies. --- Source: https://docs.versionseven.ai/api-reference/auth # Auth Check that an API key works and see which organization it belongs to. - [Verify an API key](https://docs.versionseven.ai/api-reference/auth/verify-api-key): `GET /v1/auth/verify` --- Source: https://docs.versionseven.ai/api-reference/accounts # Sender accounts The LinkedIn and email accounts connected to your organization for outreach, whether each one is still connected, and links that connect or reconnect one. - [List sender accounts](https://docs.versionseven.ai/api-reference/accounts/list-accounts): `GET /v1/accounts` - [Create a connect link](https://docs.versionseven.ai/api-reference/accounts/create-connect-link): `POST /v1/accounts/connect-link` --- Source: https://docs.versionseven.ai/api-reference/leads # Leads Create leads, enrol them in campaigns, find them with filters, update them, and remove them from campaigns. - [Create a lead](https://docs.versionseven.ai/api-reference/leads/create-lead): `POST /v1/leads` - [List leads](https://docs.versionseven.ai/api-reference/leads/list-leads): `GET /v1/leads` - [Retrieve a lead](https://docs.versionseven.ai/api-reference/leads/retrieve-lead): `GET /v1/leads/{lead_id}` - [Update a lead](https://docs.versionseven.ai/api-reference/leads/update-lead): `PATCH /v1/leads/{lead_id}` - [Delete a lead or remove it from a campaign](https://docs.versionseven.ai/api-reference/leads/remove-lead-from-campaign): `DELETE /v1/leads/{lead_id}` - [Create a standalone lead](https://docs.versionseven.ai/api-reference/leads/create-standalone-lead): `POST /v1/leads/create` (deprecated) --- Source: https://docs.versionseven.ai/api-reference/campaigns # Campaigns Create and manage campaigns: their sequences, AI fields, senders, reply agent, activation preflight, analytics, A/B tests, bookings, lead queue, and the webhooks that receive prospect replies. - [List campaigns](https://docs.versionseven.ai/api-reference/campaigns/list-campaigns): `GET /v1/campaigns` - [Create a campaign](https://docs.versionseven.ai/api-reference/campaigns/create-campaign): `POST /v1/campaigns` - [Retrieve a campaign](https://docs.versionseven.ai/api-reference/campaigns/retrieve-campaign): `GET /v1/campaigns/{campaign_id}` - [Update a campaign](https://docs.versionseven.ai/api-reference/campaigns/update-campaign): `PATCH /v1/campaigns/{campaign_id}` - [Replace a campaign's sequence](https://docs.versionseven.ai/api-reference/campaigns/replace-sequence): `PUT /v1/campaigns/{campaign_id}/sequence` - [Update a sequence step](https://docs.versionseven.ai/api-reference/campaigns/update-sequence-step): `PATCH /v1/campaigns/{campaign_id}/sequence/steps/{step_id}` - [Get campaign analytics](https://docs.versionseven.ai/api-reference/campaigns/get-campaign-analysis): `GET /v1/campaigns/{campaign_id}/analysis` - [Get a campaign's queue](https://docs.versionseven.ai/api-reference/campaigns/get-campaign-queue): `GET /v1/campaigns/{campaign_id}/queue` - [Get the activation preflight](https://docs.versionseven.ai/api-reference/campaigns/get-campaign-preflight): `GET /v1/campaigns/{campaign_id}/preflight` - [Assign sender accounts](https://docs.versionseven.ai/api-reference/campaigns/assign-senders): `PUT /v1/campaigns/{campaign_id}/senders` - [List AI fields](https://docs.versionseven.ai/api-reference/campaigns/list-ai-fields): `GET /v1/campaigns/{campaign_id}/ai-fields` - [Create an AI field](https://docs.versionseven.ai/api-reference/campaigns/create-ai-field): `POST /v1/campaigns/{campaign_id}/ai-fields` - [Update an AI field](https://docs.versionseven.ai/api-reference/campaigns/update-ai-field): `PATCH /v1/campaigns/{campaign_id}/ai-fields/{field_id}` - [Get the reply agent](https://docs.versionseven.ai/api-reference/campaigns/get-responder): `GET /v1/campaigns/{campaign_id}/responder` - [Update the reply agent](https://docs.versionseven.ai/api-reference/campaigns/update-responder): `PATCH /v1/campaigns/{campaign_id}/responder` - [Preview a campaign's messages](https://docs.versionseven.ai/api-reference/campaigns/preview-campaign): `POST /v1/campaigns/{campaign_id}/preview` - [List meetings booked](https://docs.versionseven.ai/api-reference/campaigns/list-bookings): `GET /v1/campaigns/{campaign_id}/bookings` - [Turn A/B testing on or off](https://docs.versionseven.ai/api-reference/campaigns/set-ab-testing): `POST /v1/campaigns/{campaign_id}/ab-testing` - [Promote an A/B winner](https://docs.versionseven.ai/api-reference/campaigns/promote-ab-winner): `POST /v1/campaigns/{campaign_id}/ab-testing/promote` - [Get daily stats](https://docs.versionseven.ai/api-reference/campaigns/get-daily-stats): `GET /v1/campaigns/{campaign_id}/daily-stats` - [Get the step funnel](https://docs.versionseven.ai/api-reference/campaigns/get-step-funnel): `GET /v1/campaigns/{campaign_id}/step-funnel` - [Get the sender breakdown](https://docs.versionseven.ai/api-reference/campaigns/get-sender-breakdown): `GET /v1/campaigns/{campaign_id}/senders/breakdown` - [Get personalization quality](https://docs.versionseven.ai/api-reference/campaigns/get-personalization-quality): `GET /v1/campaigns/{campaign_id}/personalization-quality` - [Compare A/B cohorts](https://docs.versionseven.ai/api-reference/campaigns/get-ab-cohorts): `GET /v1/campaigns/{campaign_id}/ab-cohorts` - [List a campaign's webhooks](https://docs.versionseven.ai/api-reference/campaigns/list-webhooks): `GET /v1/campaigns/{campaign_id}/webhooks` - [Create a webhook](https://docs.versionseven.ai/api-reference/campaigns/create-webhook): `POST /v1/campaigns/{campaign_id}/webhooks` - [Delete a webhook](https://docs.versionseven.ai/api-reference/campaigns/delete-webhook): `DELETE /v1/campaigns/{campaign_id}/webhooks/{webhook_id}` - [Set or rotate a webhook secret](https://docs.versionseven.ai/api-reference/campaigns/rotate-webhook-secret): `POST /v1/campaigns/{campaign_id}/webhooks/{webhook_id}/secret` - [Activate a webhook](https://docs.versionseven.ai/api-reference/campaigns/activate-webhook): `POST /v1/campaigns/{campaign_id}/webhook/activate` (deprecated) - [Deactivate a webhook](https://docs.versionseven.ai/api-reference/campaigns/deactivate-webhook): `POST /v1/campaigns/{campaign_id}/webhook/deactivate` (deprecated) - [List sequence templates](https://docs.versionseven.ai/api-reference/campaigns/list-sequence-templates): `GET /v1/campaigns/sequence-templates` (deprecated) - [List webhook examples](https://docs.versionseven.ai/api-reference/campaigns/list-webhook-examples): `GET /v1/campaigns/webhook/examples` (deprecated) --- Source: https://docs.versionseven.ai/api-reference/crm-pipelines # Pipelines Read and update the CRM pipelines that deals move through, along with their stages. - [List pipelines](https://docs.versionseven.ai/api-reference/crm-pipelines/list-pipelines): `GET /v1/crm/pipelines` - [Retrieve a pipeline](https://docs.versionseven.ai/api-reference/crm-pipelines/retrieve-pipeline): `GET /v1/crm/pipelines/{pipeline_id}` - [Update a pipeline](https://docs.versionseven.ai/api-reference/crm-pipelines/update-pipeline): `PATCH /v1/crm/pipelines/{pipeline_id}` --- Source: https://docs.versionseven.ai/api-reference/crm-deals # Deals Create, list, retrieve and update deals in the Victoria AI CRM. - [List deals](https://docs.versionseven.ai/api-reference/crm-deals/list-deals): `GET /v1/crm/deals` - [Create a deal](https://docs.versionseven.ai/api-reference/crm-deals/create-deal): `POST /v1/crm/deals` - [Retrieve a deal](https://docs.versionseven.ai/api-reference/crm-deals/retrieve-deal): `GET /v1/crm/deals/{deal_id}` - [Update a deal](https://docs.versionseven.ai/api-reference/crm-deals/update-deal): `PATCH /v1/crm/deals/{deal_id}` --- Source: https://docs.versionseven.ai/api-reference/reference # Reference Blank sequence templates to start from, and example payloads for building a webhook receiver. - [List sequence templates](https://docs.versionseven.ai/api-reference/reference/list-sequence-templates): `GET /v1/sequence-templates` - [List webhook examples](https://docs.versionseven.ai/api-reference/reference/list-webhook-examples): `GET /v1/webhooks/examples` --- Source: https://docs.versionseven.ai/api-reference/auth/verify-api-key # Verify an API key `GET https://api.versionseven.ai/v1/auth/verify` Confirms that an API key works and returns the organization it belongs to. It needs no scope, which makes it the right first call from a new integration. - Required scope: none ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/auth/verify" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/auth/verify", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/auth/verify", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Response ### `200` ```json { "success": true, "message": "API key is valid", "organization_id": "123e4567-e89b-12d3-a456-426614174000", "organization_name": "Acme Corporation" } ``` - `success` (boolean, required, example `true`): Whether the API key is valid - `message` (string, required, example `"API key is valid"`): Status message - `organization_id` (string, required, example `"123e4567-e89b-12d3-a456-426614174000"`): Organization ID associated with this API key - `organization_name` (string or null, optional, example `"Acme Corporation"`): Organization name ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/accounts/list-accounts # List sender accounts `GET https://api.versionseven.ai/v1/accounts` Every LinkedIn and email account connected to your organization for outreach, with its status. An account with `is_active: false` has been disconnected, usually by an authentication error, and needs to be reconnected: in the Victoria AI app, or with a [reconnect link](https://docs.versionseven.ai/api-reference/accounts/create-connect-link). - Required scope: `accounts:read` ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/accounts" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/accounts", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/accounts", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Query parameters - `limit` (integer, optional, ≥ 1, ≤ 500, default `500`): Maximum rows to return - `offset` (integer, optional, ≥ 0, default `0`): Rows to skip before returning results ## Response ### `200` ```json { "total": 1, "limit": 1, "offset": 1, "has_more": true, "accounts": [ { "account_id": "739b093a-e9fb-4013-a24b-a87a4069d38a", "created_at": "2026-01-14T19:30:00+00:00", "email": "jane@example.com", "id": "a5614527-0ce6-43be-bd1d-d012b7394867", "is_active": true, "organization_id": "073d93c2-6786-4467-a327-7342ce462caf", "platform": "string", "platform_username": "string", "system_id": "e73f154e-2c31-4392-90a5-2367f25559ec", "user_id": "f89d6b69-6045-4241-bc5b-09b4d0d8ad86" } ], "count": 0, "success": true } ``` - `total` (integer, required): Total rows matching the query, across all pages - `limit` (integer, required): Page size that was applied - `offset` (integer, required): Rows skipped before this page - `has_more` (boolean, required): Whether a further page exists - `accounts` (array of objects, optional) - `account_id` (string or null, optional): The account's ID at the messaging provider that sends on its behalf. - `created_at` (string · date-time or null, optional) - `email` (string or null, optional) - `id` (string or null, optional) - `is_active` (boolean or null, optional): false = disconnected, needs reconnect - `organization_id` (string or null, optional) - `platform` (string or null, optional): linkedin | email - `platform_username` (string or null, optional) - `system_id` (string or null, optional) - `user_id` (string or null, optional) - `count` (integer, optional, default `0`): Accounts in this page. Deprecated: use `total` - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/accounts/create-connect-link # Create a connect link `POST https://api.versionseven.ai/v1/accounts/connect-link` Mints a hosted sign-in link for a sender account. Send the `url` to the person whose LinkedIn or mailbox it is; they sign in on the hosted page, and the account appears in [List sender accounts](https://docs.versionseven.ai/api-reference/accounts/list-accounts) once connected. The link expires at `expires_at`, about a week after it's minted. Without `reconnect_account_id`, the link adds a new account (`type: "create"`), which needs a free seat for its `platform`; with none free, the request answers `409 ACCOUNT_LIMIT_REACHED`. With `reconnect_account_id`, the `id` or `account_id` of an account that has disconnected, the link re-authenticates that account (`type: "reconnect"`) and uses no seat. The account must be on the same `platform`. `email` is required when `platform` is `email`. The redirect URLs must be `https` on an origin Victoria AI allows, which always includes the Victoria AI app; leave them out to land in the app. The failure URL defaults to the success URL. - Required scope: `accounts:write` ## Request **cURL** ```bash curl -X POST "https://api.versionseven.ai/v1/accounts/connect-link" \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "email", "display_name": "Sarah Johnson", "email": "sarah.johnson@acmecorp.com" }' ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/accounts/connect-link", { method: "POST", headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "platform": "email", "display_name": "Sarah Johnson", "email": "sarah.johnson@acmecorp.com" }), }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.post( "https://api.versionseven.ai/v1/accounts/connect-link", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, json={ "platform": "email", "display_name": "Sarah Johnson", "email": "sarah.johnson@acmecorp.com", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Request body - `platform` (string, required, one of `"linkedin"`, `"email"`): Which kind of sender account to connect - `display_name` (string, required, at most 120 characters): The name the account is labelled with, usually its owner's - `email` (string or null, optional, at most 254 characters): The mailbox address; required for email - `failure_redirect_url` (string or null, optional, at most 2,000 characters): Where to send the user if connecting fails; defaults to the success URL - `reconnect_account_id` (string or null, optional, at most 120 characters): Reconnect this existing account (its id or its provider account id) instead of adding a new one - `success_redirect_url` (string or null, optional, at most 2,000 characters): Where to send the user after connecting; must be an allowed https origin ## Response ### `200` ```json { "platform": "linkedin", "url": "https://example.com", "type": "create", "expires_at": "2026-01-14T19:30:00+00:00", "success": true } ``` - `platform` (string, required, one of `"linkedin"`, `"email"`) - `url` (string, required): Open this to connect; it signs the account into this organization - `type` (string, required, one of `"create"`, `"reconnect"`) - `expires_at` (string, required): ISO timestamp after which the link stops working (about a week) - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 400 | `INVALID_ACCOUNT` | A sender account ID in the request isn't a connected account in your organization. Answered when a connected assistant assigns sender accounts to a campaign. This is a `400` rather than a `404` for the same reason as `INVALID_LEAD`. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 409 | `ACCOUNT_LIMIT_REACHED` | Connecting a new sender account would exceed the organization's seats for that platform. `details` carries the `max` and the number `used`. Free a seat, add one in the Victoria AI app, or reconnect an existing account instead. | | 413 | `PAYLOAD_TOO_LARGE` | The request body is larger than 1 MB. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `ORG_HAS_NO_MEMBERS` | The organization has no members, so the campaign can't be created. Contact support. | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/leads/create-lead # Create a lead `POST https://api.versionseven.ai/v1/leads` Creates a lead, and enrols it in a campaign when you send `campaign_id`. If a lead with the same email or LinkedIn URL already exists in your organization, that lead is enrolled instead of a new one being created. Send the lead's fields nested under `lead`, or spread across the top level of the body; both are accepted. `first_name` and `last_name` are required, along with at least one of `email` or `linkedin_url`. Emails are trimmed and matched without regard to case; LinkedIn URLs are trimmed and matched exactly. | Situation | Response | | - | - | | No matching lead | `201`, with `lead_created: true`. The lead is created, and enrolled when you sent `campaign_id`. | | A matching lead, not yet in `campaign_id` | `200`, with `lead_created: false`. The existing lead is enrolled, and `lead` is its stored record: the fields in your request don't update it. | | A matching lead already in `campaign_id` | `409 LEAD_ALREADY_IN_CAMPAIGN` | | A matching lead, and no `campaign_id` | `409 LEAD_ALREADY_EXISTS` | | The contact is on your do-not-contact list | `409 LEAD_SUPPRESSED`. Nothing is created or enrolled; `details.suppressed_by` is `email`, `domain` or `linkedin_url`. | Send an `Idempotency-Key` so a retry after a timeout returns the original response instead of a `409`. > **Note:** An enrolment starts in the campaign's backlog. The lead is contacted once the campaign has sending capacity for it, not the moment this request returns. - Required scope: `leads:write` - Accepts an `Idempotency-Key` header, which makes retries safe. See [Idempotency](https://docs.versionseven.ai/guides/idempotency). ## Request **cURL** ```bash curl -X POST "https://api.versionseven.ai/v1/leads" \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "lead": { "first_name": "Sarah", "last_name": "Johnson", "annual_revenue": 5000000, "company": "Acme Corp", "company_website": "https://acmecorp.com", "custom_fields": { "priority": "high", "source": "conference" }, "email": "sarah.johnson@acmecorp.com", "employees": 150, "industry": "Technology", "linkedin_url": "https://linkedin.com/in/sarahjohnson", "title": "VP of Sales" }, "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "custom_fields": { "utm_source": "linkedin" } }' ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/leads", { method: "POST", headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID(), }, body: JSON.stringify({ "lead": { "first_name": "Sarah", "last_name": "Johnson", "annual_revenue": 5000000, "company": "Acme Corp", "company_website": "https://acmecorp.com", "custom_fields": { "priority": "high", "source": "conference" }, "email": "sarah.johnson@acmecorp.com", "employees": 150, "industry": "Technology", "linkedin_url": "https://linkedin.com/in/sarahjohnson", "title": "VP of Sales" }, "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "custom_fields": { "utm_source": "linkedin" } }), }); const data = await response.json(); ``` **Python** ```python import os import uuid import requests response = requests.post( "https://api.versionseven.ai/v1/leads", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", "Idempotency-Key": str(uuid.uuid4()), }, json={ "lead": { "first_name": "Sarah", "last_name": "Johnson", "annual_revenue": 5000000, "company": "Acme Corp", "company_website": "https://acmecorp.com", "custom_fields": { "priority": "high", "source": "conference", }, "email": "sarah.johnson@acmecorp.com", "employees": 150, "industry": "Technology", "linkedin_url": "https://linkedin.com/in/sarahjohnson", "title": "VP of Sales", }, "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "custom_fields": { "utm_source": "linkedin", }, }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. - `Idempotency-Key` (string, optional, at most 255 characters): Any unique string, such as a UUID, that makes retrying this request safe. A repeat with the same key for at least 24 hours returns the original response instead of doing the work again. Use a new key for each distinct request. ## Request body - `lead` (object, required): Lead data. May also be supplied as top-level fields. - `first_name` (string, required, at most 2,000 characters, example `"Sarah"`): Lead's first name - `last_name` (string, required, at most 2,000 characters, example `"Johnson"`): Lead's last name - `annual_revenue` (integer or null, optional, ≥ 0, example `5000000`): Company annual revenue in dollars - `company` (string or null, optional, at most 2,000 characters, example `"Acme Corp"`): Company name - `company_website` (string or null, optional, at most 2,000 characters, example `"https://acmecorp.com"`): Lead's company website URL - `custom_fields` (object or null, optional): Custom fields as key-value pairs - `email` (string · email or null, optional, at most 2,000 characters, example `"sarah.johnson@acmecorp.com"`): Lead's email address (required if linkedin\_url not provided) - `employees` (integer or null, optional, ≥ 0, example `150`): Number of employees at the company - `industry` (string or null, optional, at most 2,000 characters, example `"Technology"`): Industry sector - `linkedin_url` (string or null, optional, at most 2,000 characters, example `"https://linkedin.com/in/sarahjohnson"`): Lead's LinkedIn profile URL (required if email not provided) - `title` (string or null, optional, at most 2,000 characters, example `"VP of Sales"`): Job title - `campaign_id` (string · uuid or null, optional, at most 2,000 characters, example `"550e8400-e29b-41d4-a716-446655440000"`): UUID of the campaign to enrol the lead in. Omit to create the lead without enrolling it. - `custom_fields` (object or null, optional): Custom fields for this campaign assignment (distinct from the lead's own custom\_fields) ## Response ### `200 / 201` ```json { "message": "Lead successfully added to campaign", "lead_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "enrollment": { "sequence_lead_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "campaign_id": "550e8400-e29b-41d4-a716-446655440000" }, "lead": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2024-01-15T10:30:00+00:00", "annual_revenue": 5000000, "company": "Acme Corp", "company_website": "https://acmecorp.com", "custom_fields": {}, "delivered_at": "2026-09-01T09:00:00+00:00", "email": "sarah.johnson@acmecorp.com", "employees": 150, "first_name": "Sarah", "industry": "Technology", "last_name": "Johnson", "linkedin_url": "https://linkedin.com/in/sarahjohnson", "phone_number": "+1-555-123-4567", "qualification_score": 85.5, "source": "lead_database", "title": "VP of Sales" }, "lead_created": true, "sequence_lead_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "success": true } ``` - `message` (string, required, example `"Lead successfully added to campaign"`) - `lead_id` (string, required, example `"a1b2c3d4-e5f6-7890-abcd-ef1234567890"`): ID of the created or existing lead - `campaign_id` (string or null, optional): Campaign the lead was enrolled in. Also available as enrollment.campaign\_id - `enrollment` (object or null, optional): Present when a campaign\_id was supplied; null otherwise - `sequence_lead_id` (string, required, example `"b2c3d4e5-f6a7-8901-bcde-f12345678901"`): ID of the campaign sequence entry - `campaign_id` (string, required, example `"550e8400-e29b-41d4-a716-446655440000"`): Campaign the lead was enrolled in - `lead` (object or null, optional): The lead record as stored - `lead_created` (boolean or null, optional, example `true`): true when this request created the lead; false when a lead with this email or LinkedIn URL already existed and was enrolled - `sequence_lead_id` (string or null, optional): ID of the campaign sequence entry. Also available as enrollment.sequence\_lead\_id - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `TRIAL_LEAD_CAP_REACHED` | The organization is on a free trial and has used up its lead quota, so the lead wasn't created. `details` carries the `cap`, the number `used` and the `remaining` count. Subscribe in the Victoria AI app to add more. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 409 | `LEAD_ALREADY_IN_CAMPAIGN` | The lead already exists in your organization and is already enrolled in the campaign given by `campaign_id`, so there's nothing to do. `details` carries the lead's `existing_lead_id`, the `campaign_id` and the enrolment's `sequence_lead_id`. | | 409 | `LEAD_ALREADY_EXISTS` | A lead with the same email or LinkedIn URL already exists in your organization. From [Create a lead](https://docs.versionseven.ai/api-reference/leads/create-lead), this means the request named no `campaign_id` to enrol the existing lead in, or raced an identical request. Changing a lead's email or LinkedIn URL to another lead's answers it too. When it's known, `details.existing_lead_id` identifies the existing lead. | | 409 | `LEAD_SUPPRESSED` | The contact is on your organization's do-not-contact list, so the lead wasn't created, enrolled or updated. `details.suppressed_by` is `email`, `domain` or `linkedin_url`, and `details.value` is the entry that matched. | | 409 | `IDEMPOTENCY_KEY_REUSED` | This `Idempotency-Key` was already used with a different request. Use a new key for each distinct request. | | 409 | `IDEMPOTENCY_IN_PROGRESS` | The first request with this `Idempotency-Key` is still running. Retry after the `Retry-After` interval. | | 413 | `PAYLOAD_TOO_LARGE` | The request body is larger than 1 MB. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`, and on `409 IDEMPOTENCY_IN_PROGRESS`: seconds to wait before retrying. | | `Idempotent-Replay` | `true` when the response is a stored replay of an earlier request with the same `Idempotency-Key`. | --- Source: https://docs.versionseven.ai/api-reference/leads/list-leads # List leads `GET https://api.versionseven.ai/v1/leads` Leads in your organization, newest first. Filter by campaign, revenue range, employee count, industry, title or a search term, and page through the results with `limit` and `offset`. - Required scope: `leads:read` ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/leads" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/leads", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/leads", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Query parameters - `campaign_id` (string · uuid or null, optional): Filter by campaign ID - `min_revenue` (integer or null, optional, ≥ 0): Minimum annual revenue filter - `max_revenue` (integer or null, optional, ≥ 0): Maximum annual revenue filter - `min_employees` (integer or null, optional, ≥ 0): Minimum employee count filter - `max_employees` (integer or null, optional, ≥ 0): Maximum employee count filter - `industry` (string or null, optional, at most 200 characters): Filter by industry (case-insensitive match) - `title` (string or null, optional, at most 200 characters): Filter by job title (case-insensitive partial match) - `search` (string or null, optional, at most 200 characters): Up to 5 words, separated by spaces. Every word must match at least one of the lead's email, first name, last name or company. - `page` (integer, optional, ≥ 1, default `1`): Page number (1-indexed). Prefer `offset`, which every list route supports - `limit` (integer, optional, ≥ 1, ≤ 100, default `20`): Results per page (max 100) - `offset` (integer or null, optional, ≥ 0): Rows to skip before this page. Takes precedence over `page` when both are sent. ## Response ### `200` ```json { "total": 1, "limit": 1, "offset": 1, "has_more": true, "leads": [ { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2024-01-15T10:30:00+00:00", "annual_revenue": 5000000, "company": "Acme Corp", "company_website": "https://acmecorp.com", "custom_fields": {}, "delivered_at": "2026-09-01T09:00:00+00:00", "email": "sarah.johnson@acmecorp.com", "employees": 150, "first_name": "Sarah", "industry": "Technology", "last_name": "Johnson", "linkedin_url": "https://linkedin.com/in/sarahjohnson", "phone_number": "+1-555-123-4567", "qualification_score": 85.5, "source": "lead_database", "title": "VP of Sales" } ], "count": 1, "page": 1, "success": true } ``` - `total` (integer, required): Total rows matching the query, across all pages - `limit` (integer, required): Page size that was applied - `offset` (integer, required): Rows skipped before this page - `has_more` (boolean, required): Whether a further page exists - `leads` (array of objects, required): Array of leads - `id` (string, required, example `"a1b2c3d4-e5f6-7890-abcd-ef1234567890"`): Lead UUID - `created_at` (string · date-time, required, example `"2024-01-15T10:30:00+00:00"`): When the lead was created - `annual_revenue` (integer or null, optional, example `5000000`): Company annual revenue in dollars - `company` (string or null, optional, example `"Acme Corp"`): Company name - `company_website` (string or null, optional, example `"https://acmecorp.com"`): Lead's company website URL - `custom_fields` (object or null, optional): Custom fields as key-value pairs - `delivered_at` (string · date-time or null, optional, example `"2026-09-01T09:00:00+00:00"`): When a lead-database result was delivered to your workspace; null for leads from other sources - `email` (string or null, optional, example `"sarah.johnson@acmecorp.com"`): Lead's email address - `employees` (integer or null, optional, example `150`): Number of employees at the company - `first_name` (string or null, optional, example `"Sarah"`): Lead's first name - `industry` (string or null, optional, example `"Technology"`): Industry sector - `last_name` (string or null, optional, example `"Johnson"`): Lead's last name - `linkedin_url` (string or null, optional, example `"https://linkedin.com/in/sarahjohnson"`): Lead's LinkedIn profile URL - `phone_number` (string or null, optional, example `"+1-555-123-4567"`): Phone number - `qualification_score` (number or null, optional, example `85.5`): Lead qualification score (0-100) - `source` (string or null, optional, example `"lead_database"`): Where the lead came from: `lead_database` for a licensed lead-database result, `csv` for an upload, `manual` for one entered by hand, `linkedin_search` for a LinkedIn search; null for leads recorded before sources were tracked - `title` (string or null, optional, example `"VP of Sales"`): Job title - `count` (integer, required): Leads in this page - `page` (integer, required): Current page number. Prefer `offset`, which every list route returns - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/leads/retrieve-lead # Retrieve a lead `GET https://api.versionseven.ai/v1/leads/{lead_id}` Retrieve detailed information about a specific lead including contact info, company details, and custom fields. - Required scope: `leads:read` ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/leads/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/leads/a1b2c3d4-e5f6-7890-abcd-ef1234567890", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/leads/a1b2c3d4-e5f6-7890-abcd-ef1234567890", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `lead_id` (string · uuid, required): ID of the lead. ## Response ### `200` ```json { "lead": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2024-01-15T10:30:00+00:00", "annual_revenue": 5000000, "company": "Acme Corp", "company_website": "https://acmecorp.com", "custom_fields": {}, "delivered_at": "2026-09-01T09:00:00+00:00", "email": "sarah.johnson@acmecorp.com", "employees": 150, "first_name": "Sarah", "industry": "Technology", "last_name": "Johnson", "linkedin_url": "https://linkedin.com/in/sarahjohnson", "phone_number": "+1-555-123-4567", "qualification_score": 85.5, "source": "lead_database", "title": "VP of Sales" }, "success": true } ``` - `lead` (object, required): Lead details - `id` (string, required, example `"a1b2c3d4-e5f6-7890-abcd-ef1234567890"`): Lead UUID - `created_at` (string · date-time, required, example `"2024-01-15T10:30:00+00:00"`): When the lead was created - `annual_revenue` (integer or null, optional, example `5000000`): Company annual revenue in dollars - `company` (string or null, optional, example `"Acme Corp"`): Company name - `company_website` (string or null, optional, example `"https://acmecorp.com"`): Lead's company website URL - `custom_fields` (object or null, optional): Custom fields as key-value pairs - `delivered_at` (string · date-time or null, optional, example `"2026-09-01T09:00:00+00:00"`): When a lead-database result was delivered to your workspace; null for leads from other sources - `email` (string or null, optional, example `"sarah.johnson@acmecorp.com"`): Lead's email address - `employees` (integer or null, optional, example `150`): Number of employees at the company - `first_name` (string or null, optional, example `"Sarah"`): Lead's first name - `industry` (string or null, optional, example `"Technology"`): Industry sector - `last_name` (string or null, optional, example `"Johnson"`): Lead's last name - `linkedin_url` (string or null, optional, example `"https://linkedin.com/in/sarahjohnson"`): Lead's LinkedIn profile URL - `phone_number` (string or null, optional, example `"+1-555-123-4567"`): Phone number - `qualification_score` (number or null, optional, example `85.5`): Lead qualification score (0-100) - `source` (string or null, optional, example `"lead_database"`): Where the lead came from: `lead_database` for a licensed lead-database result, `csv` for an upload, `manual` for one entered by hand, `linkedin_search` for a LinkedIn search; null for leads recorded before sources were tracked - `title` (string or null, optional, example `"VP of Sales"`): Job title - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `LEAD_NOT_FOUND` | No lead with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/leads/update-lead # Update a lead `PATCH https://api.versionseven.ai/v1/leads/{lead_id}` Updates the fields you send and leaves the rest unchanged. An email is saved trimmed and lowercased. Changing the email or LinkedIn URL to one another lead already has answers `409 LEAD_ALREADY_EXISTS`, and to one on your do-not-contact list answers `409 LEAD_SUPPRESSED`. > **Note:** Fields sent as `null` are ignored, so this endpoint can't clear a field. `custom_fields` replaces the lead's whole custom fields object rather than merging into it. - Required scope: `leads:write` ## Request **cURL** ```bash curl -X PATCH "https://api.versionseven.ai/v1/leads/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "annual_revenue": 5000000, "company": "Acme Corp", "company_website": "https://acmecorp.com", "custom_fields": { "source": "conference" }, "email": "sarah.johnson@acmecorp.com", "employees": 150, "first_name": "Sarah", "industry": "Technology", "last_name": "Johnson", "linkedin_url": "https://linkedin.com/in/sarahjohnson", "phone_number": "+1-555-123-4567", "qualification_score": 85.5, "title": "VP of Sales" }' ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/leads/a1b2c3d4-e5f6-7890-abcd-ef1234567890", { method: "PATCH", headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "annual_revenue": 5000000, "company": "Acme Corp", "company_website": "https://acmecorp.com", "custom_fields": { "source": "conference" }, "email": "sarah.johnson@acmecorp.com", "employees": 150, "first_name": "Sarah", "industry": "Technology", "last_name": "Johnson", "linkedin_url": "https://linkedin.com/in/sarahjohnson", "phone_number": "+1-555-123-4567", "qualification_score": 85.5, "title": "VP of Sales" }), }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.patch( "https://api.versionseven.ai/v1/leads/a1b2c3d4-e5f6-7890-abcd-ef1234567890", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, json={ "annual_revenue": 5000000, "company": "Acme Corp", "company_website": "https://acmecorp.com", "custom_fields": { "source": "conference", }, "email": "sarah.johnson@acmecorp.com", "employees": 150, "first_name": "Sarah", "industry": "Technology", "last_name": "Johnson", "linkedin_url": "https://linkedin.com/in/sarahjohnson", "phone_number": "+1-555-123-4567", "qualification_score": 85.5, "title": "VP of Sales", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `lead_id` (string · uuid, required): ID of the lead. ## Request body - `annual_revenue` (integer or null, optional, ≥ 0, example `5000000`): Company annual revenue in dollars - `company` (string or null, optional, at most 2,000 characters, example `"Acme Corp"`): Company name - `company_website` (string or null, optional, at most 2,000 characters, example `"https://acmecorp.com"`): Lead's company website URL - `custom_fields` (object or null, optional): Custom fields as key-value pairs - `email` (string · email or null, optional, at most 2,000 characters, example `"sarah.johnson@acmecorp.com"`): Lead's email address - `employees` (integer or null, optional, ≥ 0, example `150`): Number of employees at the company - `first_name` (string or null, optional, at most 2,000 characters, example `"Sarah"`): Lead's first name - `industry` (string or null, optional, at most 2,000 characters, example `"Technology"`): Industry sector - `last_name` (string or null, optional, at most 2,000 characters, example `"Johnson"`): Lead's last name - `linkedin_url` (string or null, optional, at most 2,000 characters, example `"https://linkedin.com/in/sarahjohnson"`): Lead's LinkedIn profile URL - `phone_number` (string or null, optional, at most 2,000 characters, example `"+1-555-123-4567"`): Phone number - `qualification_score` (number or null, optional, example `85.5`): Lead qualification score (0-100) - `title` (string or null, optional, at most 2,000 characters, example `"VP of Sales"`): Job title ## Response ### `200` ```json { "message": "string", "lead": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2024-01-15T10:30:00+00:00", "annual_revenue": 5000000, "company": "Acme Corp", "company_website": "https://acmecorp.com", "custom_fields": {}, "delivered_at": "2026-09-01T09:00:00+00:00", "email": "sarah.johnson@acmecorp.com", "employees": 150, "first_name": "Sarah", "industry": "Technology", "last_name": "Johnson", "linkedin_url": "https://linkedin.com/in/sarahjohnson", "phone_number": "+1-555-123-4567", "qualification_score": 85.5, "source": "lead_database", "title": "VP of Sales" }, "success": true } ``` - `message` (string, required): Success message - `lead` (object, required): Updated lead details - `id` (string, required, example `"a1b2c3d4-e5f6-7890-abcd-ef1234567890"`): Lead UUID - `created_at` (string · date-time, required, example `"2024-01-15T10:30:00+00:00"`): When the lead was created - `annual_revenue` (integer or null, optional, example `5000000`): Company annual revenue in dollars - `company` (string or null, optional, example `"Acme Corp"`): Company name - `company_website` (string or null, optional, example `"https://acmecorp.com"`): Lead's company website URL - `custom_fields` (object or null, optional): Custom fields as key-value pairs - `delivered_at` (string · date-time or null, optional, example `"2026-09-01T09:00:00+00:00"`): When a lead-database result was delivered to your workspace; null for leads from other sources - `email` (string or null, optional, example `"sarah.johnson@acmecorp.com"`): Lead's email address - `employees` (integer or null, optional, example `150`): Number of employees at the company - `first_name` (string or null, optional, example `"Sarah"`): Lead's first name - `industry` (string or null, optional, example `"Technology"`): Industry sector - `last_name` (string or null, optional, example `"Johnson"`): Lead's last name - `linkedin_url` (string or null, optional, example `"https://linkedin.com/in/sarahjohnson"`): Lead's LinkedIn profile URL - `phone_number` (string or null, optional, example `"+1-555-123-4567"`): Phone number - `qualification_score` (number or null, optional, example `85.5`): Lead qualification score (0-100) - `source` (string or null, optional, example `"lead_database"`): Where the lead came from: `lead_database` for a licensed lead-database result, `csv` for an upload, `manual` for one entered by hand, `linkedin_search` for a LinkedIn search; null for leads recorded before sources were tracked - `title` (string or null, optional, example `"VP of Sales"`): Job title - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 400 | `NO_UPDATES` | The update request contained no fields to change. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `LEAD_NOT_FOUND` | No lead with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 409 | `LEAD_ALREADY_EXISTS` | A lead with the same email or LinkedIn URL already exists in your organization. From [Create a lead](https://docs.versionseven.ai/api-reference/leads/create-lead), this means the request named no `campaign_id` to enrol the existing lead in, or raced an identical request. Changing a lead's email or LinkedIn URL to another lead's answers it too. When it's known, `details.existing_lead_id` identifies the existing lead. | | 409 | `LEAD_SUPPRESSED` | The contact is on your organization's do-not-contact list, so the lead wasn't created, enrolled or updated. `details.suppressed_by` is `email`, `domain` or `linkedin_url`, and `details.value` is the entry that matched. | | 413 | `PAYLOAD_TOO_LARGE` | The request body is larger than 1 MB. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/leads/remove-lead-from-campaign # Delete a lead or remove it from a campaign `DELETE https://api.versionseven.ai/v1/leads/{lead_id}` With the `campaign_id` query parameter, removes the lead from that campaign and leaves its record unchanged; a lead that isn't enrolled there answers `404 LEAD_NOT_IN_CAMPAIGN`. Without `campaign_id`, deletes the lead itself. Its personal and company data is erased in place and it disappears from every read, while an anonymised row remains as the record of the activity that happened. > **Warning:** Omitting `campaign_id` deletes the lead, not just an enrolment, and the erasure can't be undone. To take a lead out of one campaign, always send `campaign_id`. - Required scope: `leads:write` ## Request **cURL** ```bash curl -X DELETE "https://api.versionseven.ai/v1/leads/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/leads/a1b2c3d4-e5f6-7890-abcd-ef1234567890", { method: "DELETE", headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.delete( "https://api.versionseven.ai/v1/leads/a1b2c3d4-e5f6-7890-abcd-ef1234567890", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `lead_id` (string · uuid, required): ID of the lead. ## Query parameters - `campaign_id` (string · uuid or null, optional): UUID of the campaign to remove the lead from. Omit to delete the lead itself ## Response ### `200` ```json { "message": "string", "lead_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "success": true } ``` - `message` (string, required): Success message - `lead_id` (string, required): ID of the deleted or removed lead - `campaign_id` (string or null, optional): ID of the campaign the lead was removed from; null when the lead itself was deleted - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `LEAD_NOT_FOUND` | No lead with this ID exists in your organization. | | 404 | `LEAD_NOT_IN_CAMPAIGN` | The lead isn't enrolled in the campaign given by `campaign_id`. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/list-campaigns # List campaigns `GET https://api.versionseven.ai/v1/campaigns` Campaigns in your organization, newest first. Paginated with `limit` and `offset`: keep requesting while `has_more` is `true`. - Required scope: `campaigns:read` ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/campaigns" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/campaigns", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Query parameters - `limit` (integer, optional, ≥ 1, ≤ 500, default `500`): Maximum rows to return - `offset` (integer, optional, ≥ 0, default `0`): Rows to skip before returning results ## Response ### `200` ```json { "total": 1, "limit": 1, "offset": 1, "has_more": true, "message": "string", "campaigns": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "description": "Targeting enterprise accounts in the tech sector", "is_active": true, "name": "Q1 Enterprise Outreach", "organization_id": "123e4567-e89b-12d3-a456-426614174000", "refill_policy": {}, "user_id": "987fcdeb-51a2-4d4e-b567-890123456789" } ], "count": 1, "success": true } ``` - `total` (integer, required): Total rows matching the query, across all pages - `limit` (integer, required): Page size that was applied - `offset` (integer, required): Rows skipped before this page - `has_more` (boolean, required): Whether a further page exists - `message` (string, required) - `campaigns` (array of objects, required): List of campaigns found - `id` (string, required, example `"550e8400-e29b-41d4-a716-446655440000"`): Campaign UUID - `description` (string or null, optional, example `"Targeting enterprise accounts in the tech sector"`): Campaign description - `is_active` (boolean or null, optional, example `true`): Whether the campaign is currently active - `name` (string or null, optional, example `"Q1 Enterprise Outreach"`): Campaign name - `organization_id` (string or null, optional, example `"123e4567-e89b-12d3-a456-426614174000"`): Organization ID that owns this campaign - `refill_policy` (object or null, optional): Automatic lead-refill settings for the campaign (`enabled`, `min_backlog`, `max_add_per_run`, …). `null` means low queues are reported but not refilled. - `user_id` (string or null, optional, example `"987fcdeb-51a2-4d4e-b567-890123456789"`): User ID that created this campaign - `count` (integer, required): Campaigns in this page. Deprecated: use `total` - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/create-campaign # Create a campaign `POST https://api.versionseven.ai/v1/campaigns` Creates a campaign in draft, with `is_active: false`. Add its sequence, AI fields, senders and leads, check it with [Get the activation preflight](https://docs.versionseven.ai/api-reference/campaigns/get-campaign-preflight), then activate it with [Update a campaign](https://docs.versionseven.ai/api-reference/campaigns/update-campaign). [Set up a campaign](https://docs.versionseven.ai/guides/set-up-a-campaign) walks through the order. An optional `sequence` can be sent now. A sequence is validated before it's saved: - `variation_a` is required and `variation_b` is optional. Each is a list of steps. - Step types are `view_profile`, `linkedin_connection`, `conditional`, `linkedin_message` and `email`. - A `linkedin_connection` step must be followed immediately by a `conditional` step with `branches.yes` and `branches.no`. Conditionals nest at most two deep. - `linkedin_message` steps are only allowed inside a `yes` branch. - `email` steps need `subject` and `content`. - `delay` is a number of days from 0 to 90. - Every step needs a unique integer `id`, and a sequence has at most 40 steps. The easiest valid starting point is a [sequence template](https://docs.versionseven.ai/api-reference/reference/list-sequence-templates). - Required scope: `campaigns:write` - Accepts an `Idempotency-Key` header, which makes retries safe. See [Idempotency](https://docs.versionseven.ai/guides/idempotency). ## Request **cURL** ```bash curl -X POST "https://api.versionseven.ai/v1/campaigns" \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "name": "Q3 outbound: heads of sales", "description": "Three-email sequence", "sequence": { "variation_a": [ { "id": 1, "type": "email", "subject": "Question about {company}", "content": "Hi {first_name},\n\nAre you the right person to talk to about how {company} books sales meetings?\n\nThanks,\nJane", "delay": 2 }, { "id": 2, "type": "email", "subject": "Re: Question about {company}", "content": "Hi {first_name},\n\nFollowing up on my last note. Would a short call next week be useful?\n\nJane", "delay": 3 }, { "id": 3, "type": "email", "subject": "Closing the loop", "content": "Hi {first_name},\n\nI haven'\''t heard back, so I'\''ll assume the timing isn'\''t right. If that changes, just reply to this email.\n\nJane", "delay": 0 } ], "variation_b": [] } }' ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns", { method: "POST", headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID(), }, body: JSON.stringify({ "name": "Q3 outbound: heads of sales", "description": "Three-email sequence", "sequence": { "variation_a": [ { "id": 1, "type": "email", "subject": "Question about {company}", "content": "Hi {first_name},\n\nAre you the right person to talk to about how {company} books sales meetings?\n\nThanks,\nJane", "delay": 2 }, { "id": 2, "type": "email", "subject": "Re: Question about {company}", "content": "Hi {first_name},\n\nFollowing up on my last note. Would a short call next week be useful?\n\nJane", "delay": 3 }, { "id": 3, "type": "email", "subject": "Closing the loop", "content": "Hi {first_name},\n\nI haven't heard back, so I'll assume the timing isn't right. If that changes, just reply to this email.\n\nJane", "delay": 0 } ], "variation_b": [] } }), }); const data = await response.json(); ``` **Python** ```python import os import uuid import requests response = requests.post( "https://api.versionseven.ai/v1/campaigns", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", "Idempotency-Key": str(uuid.uuid4()), }, json={ "name": "Q3 outbound: heads of sales", "description": "Three-email sequence", "sequence": { "variation_a": [ { "id": 1, "type": "email", "subject": "Question about {company}", "content": "Hi {first_name},\n\nAre you the right person to talk to about how {company} books sales meetings?\n\nThanks,\nJane", "delay": 2, }, { "id": 2, "type": "email", "subject": "Re: Question about {company}", "content": "Hi {first_name},\n\nFollowing up on my last note. Would a short call next week be useful?\n\nJane", "delay": 3, }, { "id": 3, "type": "email", "subject": "Closing the loop", "content": "Hi {first_name},\n\nI haven't heard back, so I'll assume the timing isn't right. If that changes, just reply to this email.\n\nJane", "delay": 0, }, ], "variation_b": [], }, }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. - `Idempotency-Key` (string, optional, at most 255 characters): Any unique string, such as a UUID, that makes retrying this request safe. A repeat with the same key for at least 24 hours returns the original response instead of doing the work again. Use a new key for each distinct request. ## Request body - `name` (string, required, at most 2,000 characters, example `"Q3 Proof First Outreach"`): Campaign name - `description` (string or null, optional, at most 2,000 characters, default `""`): Campaign description - `sequence` (object or null, optional): Optional full sequence object (variation\_a / variation\_b step arrays); validated against structural rules ## Response ### `201` ```json { "campaign": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Q1 Enterprise Outreach", "is_active": true, "created_at": "2024-01-15T10:30:00+00:00", "updated_at": "2024-01-20T14:45:00+00:00", "ab_testing_enabled": true, "custom_fields": null, "description": "Targeting enterprise accounts in the tech sector", "enabled_accounts": [ "string" ], "lead_database_filters": {}, "refill_policy": {}, "responder_enabled": true, "sequence": { "variation_a": [ { "id": 1, "type": "email", "subject": "Question about {company}", "content": "Hi {first_name},\n\nAre you the right person to talk to about how {company} books sales meetings?\n\nThanks,\nJane", "delay": 2 }, { "id": 2, "type": "email", "subject": "Re: Question about {company}", "content": "Hi {first_name},\n\nFollowing up on my last note. Would a short call next week be useful?\n\nJane", "delay": 3 }, { "id": 3, "type": "email", "subject": "Closing the loop", "content": "Hi {first_name},\n\nI haven't heard back, so I'll assume the timing isn't right. If that changes, just reply to this email.\n\nJane", "delay": 0 } ], "variation_b": [] }, "traffic_split": 50 }, "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "message": "Campaign created in draft state", "success": true } ``` - `campaign` (object or null, optional): The created campaign - `id` (string, required, example `"550e8400-e29b-41d4-a716-446655440000"`): Campaign UUID - `name` (string, required, example `"Q1 Enterprise Outreach"`): Campaign name - `is_active` (boolean, required, example `true`): Whether the campaign is currently active - `created_at` (string · date-time, required, example `"2024-01-15T10:30:00+00:00"`): When the campaign was created - `updated_at` (string · date-time, required, example `"2024-01-20T14:45:00+00:00"`): When the campaign was last updated - `ab_testing_enabled` (boolean or null, optional, example `true`): Whether A/B testing is enabled for message variations - `custom_fields` (any or null, optional): Custom fields — object on newer campaigns, list on legacy ones - `description` (string or null, optional, example `"Targeting enterprise accounts in the tech sector"`): Campaign description - `enabled_accounts` (array of strings or null, optional): List of connected account IDs used for outreach - `lead_database_filters` (object or null, optional): Lead-database search filters defining this campaign's ICP (same shape the lead database search accepts) - `refill_policy` (object or null, optional): Automatic lead-refill settings for the campaign: `enabled`, `min_backlog`, `target_backlog`, `max_add_per_run`, `max_tokens_per_run`, `cooldown_hours` and `mode`. - `responder_enabled` (boolean or null, optional, example `true`): Whether AI auto-responder is enabled - `sequence` (any or null, optional): Campaign sequence: an object with variation\_a and variation\_b message templates, or an empty list on a campaign with no sequence yet - `traffic_split` (integer or null, optional, example `50`): Traffic split percentage for A/B testing (0-100) - `campaign_id` (string or null, optional): UUID of the created campaign - `message` (string, optional, default `"Campaign created in draft state"`) - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 400 | `SEQUENCE_VALIDATION_FAILED` | The sequence breaks a structural rule, such as an unknown step type or a `linkedin_connection` step without a `conditional` after it. The message lists every rule that failed. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 409 | `IDEMPOTENCY_KEY_REUSED` | This `Idempotency-Key` was already used with a different request. Use a new key for each distinct request. | | 409 | `IDEMPOTENCY_IN_PROGRESS` | The first request with this `Idempotency-Key` is still running. Retry after the `Retry-After` interval. | | 413 | `PAYLOAD_TOO_LARGE` | The request body is larger than 1 MB. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `ORG_HAS_NO_MEMBERS` | The organization has no members, so the campaign can't be created. Contact support. | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`, and on `409 IDEMPOTENCY_IN_PROGRESS`: seconds to wait before retrying. | | `Idempotent-Replay` | `true` when the response is a stored replay of an earlier request with the same `Idempotency-Key`. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/retrieve-campaign # Retrieve a campaign `GET https://api.versionseven.ai/v1/campaigns/{campaign_id}` A campaign's configuration, status and sequence. - Required scope: `campaigns:read` ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. ## Response ### `200` ```json { "campaign": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Q1 Enterprise Outreach", "is_active": true, "created_at": "2024-01-15T10:30:00+00:00", "updated_at": "2024-01-20T14:45:00+00:00", "ab_testing_enabled": true, "custom_fields": null, "description": "Targeting enterprise accounts in the tech sector", "enabled_accounts": [ "string" ], "lead_database_filters": {}, "refill_policy": {}, "responder_enabled": true, "sequence": { "variation_a": [ { "id": 1, "type": "email", "subject": "Question about {company}", "content": "Hi {first_name},\n\nAre you the right person to talk to about how {company} books sales meetings?\n\nThanks,\nJane", "delay": 2 }, { "id": 2, "type": "email", "subject": "Re: Question about {company}", "content": "Hi {first_name},\n\nFollowing up on my last note. Would a short call next week be useful?\n\nJane", "delay": 3 }, { "id": 3, "type": "email", "subject": "Closing the loop", "content": "Hi {first_name},\n\nI haven't heard back, so I'll assume the timing isn't right. If that changes, just reply to this email.\n\nJane", "delay": 0 } ], "variation_b": [] }, "traffic_split": 50 }, "success": true } ``` - `campaign` (object, required): Campaign details - `id` (string, required, example `"550e8400-e29b-41d4-a716-446655440000"`): Campaign UUID - `name` (string, required, example `"Q1 Enterprise Outreach"`): Campaign name - `is_active` (boolean, required, example `true`): Whether the campaign is currently active - `created_at` (string · date-time, required, example `"2024-01-15T10:30:00+00:00"`): When the campaign was created - `updated_at` (string · date-time, required, example `"2024-01-20T14:45:00+00:00"`): When the campaign was last updated - `ab_testing_enabled` (boolean or null, optional, example `true`): Whether A/B testing is enabled for message variations - `custom_fields` (any or null, optional): Custom fields — object on newer campaigns, list on legacy ones - `description` (string or null, optional, example `"Targeting enterprise accounts in the tech sector"`): Campaign description - `enabled_accounts` (array of strings or null, optional): List of connected account IDs used for outreach - `lead_database_filters` (object or null, optional): Lead-database search filters defining this campaign's ICP (same shape the lead database search accepts) - `refill_policy` (object or null, optional): Automatic lead-refill settings for the campaign: `enabled`, `min_backlog`, `target_backlog`, `max_add_per_run`, `max_tokens_per_run`, `cooldown_hours` and `mode`. - `responder_enabled` (boolean or null, optional, example `true`): Whether AI auto-responder is enabled - `sequence` (any or null, optional): Campaign sequence: an object with variation\_a and variation\_b message templates, or an empty list on a campaign with no sequence yet - `traffic_split` (integer or null, optional, example `50`): Traffic split percentage for A/B testing (0-100) - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/update-campaign # Update a campaign `PATCH https://api.versionseven.ai/v1/campaigns/{campaign_id}` Change a campaign's `name` or `description`, activate or pause it with `is_active`, or turn the AI Appointment Setter on or off with `responder_enabled`. Fields you don't send are left unchanged. > **Note:** Activating a campaign runs the readiness preflight first: the sequence and its step content, its variables and lead data, the assigned sender accounts, sender email authentication and the subscription. A blocking check answers `409 CAMPAIGN_NOT_READY`, with every check listed in `details.checks`. Warnings alone answer `409 CAMPAIGN_ACTIVATION_WARNINGS` until you repeat the request with `ack_warnings: true`. Pausing never runs the preflight. To see the verdict without activating, call [Get the activation preflight](https://docs.versionseven.ai/api-reference/campaigns/get-campaign-preflight). - Required scope: `campaigns:write` ## Request **cURL** ```bash curl -X PATCH "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000" \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000", { method: "PATCH", headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({}), }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.patch( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, json={}, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. ## Request body - `ack_warnings` (boolean or null, optional, default `false`): With is\_active=true: accept the non-blocking warnings the preflight reported (a CAMPAIGN\_ACTIVATION\_WARNINGS response lists them under details.checks) and activate anyway. - `description` (string or null, optional, at most 2,000 characters): New description - `is_active` (boolean or null, optional): Activate (true) or pause (false) the campaign. Activation runs the readiness preflight: a blocking check answers 409 CAMPAIGN\_NOT\_READY, warnings alone answer 409 CAMPAIGN\_ACTIVATION\_WARNINGS until acknowledged with ack\_warnings. - `lead_database_filters` (object or null, optional): Replace the campaign's saved lead-database ICP filters - `name` (string or null, optional, at most 2,000 characters): New campaign name - `refill_policy` (object or null, optional): Replace the auto-refill policy (send {enabled:false} to disable; null is ignored) - `responder_enabled` (boolean or null, optional): Toggle the AI auto-responder ## Response ### `200` ```json { "campaign": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Q1 Enterprise Outreach", "is_active": true, "created_at": "2024-01-15T10:30:00+00:00", "updated_at": "2024-01-20T14:45:00+00:00", "ab_testing_enabled": true, "custom_fields": null, "description": "Targeting enterprise accounts in the tech sector", "enabled_accounts": [ "string" ], "lead_database_filters": {}, "refill_policy": {}, "responder_enabled": true, "sequence": { "variation_a": [ { "id": 1, "type": "email", "subject": "Question about {company}", "content": "Hi {first_name},\n\nAre you the right person to talk to about how {company} books sales meetings?\n\nThanks,\nJane", "delay": 2 }, { "id": 2, "type": "email", "subject": "Re: Question about {company}", "content": "Hi {first_name},\n\nFollowing up on my last note. Would a short call next week be useful?\n\nJane", "delay": 3 }, { "id": 3, "type": "email", "subject": "Closing the loop", "content": "Hi {first_name},\n\nI haven't heard back, so I'll assume the timing isn't right. If that changes, just reply to this email.\n\nJane", "delay": 0 } ], "variation_b": [] }, "traffic_split": 50 }, "message": "Campaign updated", "success": true } ``` - `campaign` (object or null, optional): The updated campaign - `id` (string, required, example `"550e8400-e29b-41d4-a716-446655440000"`): Campaign UUID - `name` (string, required, example `"Q1 Enterprise Outreach"`): Campaign name - `is_active` (boolean, required, example `true`): Whether the campaign is currently active - `created_at` (string · date-time, required, example `"2024-01-15T10:30:00+00:00"`): When the campaign was created - `updated_at` (string · date-time, required, example `"2024-01-20T14:45:00+00:00"`): When the campaign was last updated - `ab_testing_enabled` (boolean or null, optional, example `true`): Whether A/B testing is enabled for message variations - `custom_fields` (any or null, optional): Custom fields — object on newer campaigns, list on legacy ones - `description` (string or null, optional, example `"Targeting enterprise accounts in the tech sector"`): Campaign description - `enabled_accounts` (array of strings or null, optional): List of connected account IDs used for outreach - `lead_database_filters` (object or null, optional): Lead-database search filters defining this campaign's ICP (same shape the lead database search accepts) - `refill_policy` (object or null, optional): Automatic lead-refill settings for the campaign: `enabled`, `min_backlog`, `target_backlog`, `max_add_per_run`, `max_tokens_per_run`, `cooldown_hours` and `mode`. - `responder_enabled` (boolean or null, optional, example `true`): Whether AI auto-responder is enabled - `sequence` (any or null, optional): Campaign sequence: an object with variation\_a and variation\_b message templates, or an empty list on a campaign with no sequence yet - `traffic_split` (integer or null, optional, example `50`): Traffic split percentage for A/B testing (0-100) - `message` (string, optional, default `"Campaign updated"`) - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 409 | `CAMPAIGN_NOT_READY` | The campaign can't be activated: at least one readiness check is blocking, such as a step missing content, a variable with no personalization field, an unassigned or unauthenticated sender, or no enrolled leads. `details.checks` lists every check with its status. | | 409 | `CAMPAIGN_ACTIVATION_WARNINGS` | The campaign's readiness checks passed with warnings only. `details.checks` lists them. Repeat the request with `ack_warnings: true` to activate anyway. | | 413 | `PAYLOAD_TOO_LARGE` | The request body is larger than 1 MB. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/replace-sequence # Replace a campaign's sequence `PUT https://api.versionseven.ai/v1/campaigns/{campaign_id}/sequence` Replaces the campaign's whole sequence. To change one step, use [Update a sequence step](https://docs.versionseven.ai/api-reference/campaigns/update-sequence-step) instead. Send `expected_updated_at`, the campaign's `updated_at` as you last read it, to write only if nothing has changed the campaign since. If it has, nothing is written and the request answers `409 SEQUENCE_MODIFIED`; retrieve the campaign again and retry. Without it, the write always lands. A sequence is validated before it's saved: - `variation_a` is required and `variation_b` is optional. Each is a list of steps. - Step types are `view_profile`, `linkedin_connection`, `conditional`, `linkedin_message` and `email`. - A `linkedin_connection` step must be followed immediately by a `conditional` step with `branches.yes` and `branches.no`. Conditionals nest at most two deep. - `linkedin_message` steps are only allowed inside a `yes` branch. - `email` steps need `subject` and `content`. - `delay` is a number of days from 0 to 90. - Every step needs a unique integer `id`, and a sequence has at most 40 steps. The easiest valid starting point is a [sequence template](https://docs.versionseven.ai/api-reference/reference/list-sequence-templates). - Required scope: `campaigns:write` ## Request **cURL** ```bash curl -X PUT "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/sequence" \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "sequence": { "variation_a": [ { "id": 1, "type": "view_profile", "delay": 1 }, { "id": 2, "type": "linkedin_connection", "message": "Hi {first_name}, I work with sales teams and would like to connect.", "delay": 5 }, { "id": 3, "type": "conditional", "conditionalType": "linkedin_connected", "isConditional": true, "delay": 5, "branches": { "yes": [ { "id": 4, "type": "linkedin_message", "message": "Thanks for connecting, {first_name}. Are you the right person to talk to about how {company} books sales meetings?", "delay": 3 }, { "id": 5, "type": "email", "subject": "Following up, {first_name}", "content": "Hi {first_name},\n\nI sent you a note on LinkedIn. Would a short call next week be useful?\n\nJane", "delay": 0 } ], "no": [ { "id": 6, "type": "email", "subject": "Question about {company}", "content": "Hi {first_name},\n\nAre you the right person to talk to about how {company} books sales meetings?\n\nThanks,\nJane", "delay": 3 }, { "id": 7, "type": "email", "subject": "Closing the loop", "content": "Hi {first_name},\n\nI haven'\''t heard back, so I'\''ll assume the timing isn'\''t right. If that changes, just reply to this email.\n\nJane", "delay": 0 } ] } } ], "variation_b": [] } }' ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/sequence", { method: "PUT", headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "sequence": { "variation_a": [ { "id": 1, "type": "view_profile", "delay": 1 }, { "id": 2, "type": "linkedin_connection", "message": "Hi {first_name}, I work with sales teams and would like to connect.", "delay": 5 }, { "id": 3, "type": "conditional", "conditionalType": "linkedin_connected", "isConditional": true, "delay": 5, "branches": { "yes": [ { "id": 4, "type": "linkedin_message", "message": "Thanks for connecting, {first_name}. Are you the right person to talk to about how {company} books sales meetings?", "delay": 3 }, { "id": 5, "type": "email", "subject": "Following up, {first_name}", "content": "Hi {first_name},\n\nI sent you a note on LinkedIn. Would a short call next week be useful?\n\nJane", "delay": 0 } ], "no": [ { "id": 6, "type": "email", "subject": "Question about {company}", "content": "Hi {first_name},\n\nAre you the right person to talk to about how {company} books sales meetings?\n\nThanks,\nJane", "delay": 3 }, { "id": 7, "type": "email", "subject": "Closing the loop", "content": "Hi {first_name},\n\nI haven't heard back, so I'll assume the timing isn't right. If that changes, just reply to this email.\n\nJane", "delay": 0 } ] } } ], "variation_b": [] } }), }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.put( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/sequence", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, json={ "sequence": { "variation_a": [ { "id": 1, "type": "view_profile", "delay": 1, }, { "id": 2, "type": "linkedin_connection", "message": "Hi {first_name}, I work with sales teams and would like to connect.", "delay": 5, }, { "id": 3, "type": "conditional", "conditionalType": "linkedin_connected", "isConditional": True, "delay": 5, "branches": { "yes": [ { "id": 4, "type": "linkedin_message", "message": "Thanks for connecting, {first_name}. Are you the right person to talk to about how {company} books sales meetings?", "delay": 3, }, { "id": 5, "type": "email", "subject": "Following up, {first_name}", "content": "Hi {first_name},\n\nI sent you a note on LinkedIn. Would a short call next week be useful?\n\nJane", "delay": 0, }, ], "no": [ { "id": 6, "type": "email", "subject": "Question about {company}", "content": "Hi {first_name},\n\nAre you the right person to talk to about how {company} books sales meetings?\n\nThanks,\nJane", "delay": 3, }, { "id": 7, "type": "email", "subject": "Closing the loop", "content": "Hi {first_name},\n\nI haven't heard back, so I'll assume the timing isn't right. If that changes, just reply to this email.\n\nJane", "delay": 0, }, ], }, }, ], "variation_b": [], }, }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. ## Request body - `sequence` (object, required): Full sequence object with variation\_a (and optionally variation\_b) step arrays - `expected_updated_at` (string or null, optional, at most 2,000 characters): The campaign's updated\_at as you last read it. If it has changed since, nothing is written and the answer is 409 SEQUENCE\_MODIFIED. ## Response ### `200` ```json { "message": "string", "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "success": true } ``` - `message` (string, required) - `campaign_id` (string or null, optional) - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 400 | `SEQUENCE_VALIDATION_FAILED` | The sequence breaks a structural rule, such as an unknown step type or a `linkedin_connection` step without a `conditional` after it. The message lists every rule that failed. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 409 | `SEQUENCE_MODIFIED` | The sequence changed after it was read. Retrieve the campaign again and retry. | | 413 | `PAYLOAD_TOO_LARGE` | The request body is larger than 1 MB. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/update-sequence-step # Update a sequence step `PATCH https://api.versionseven.ai/v1/campaigns/{campaign_id}/sequence/steps/{step_id}` Changes one step's `message`, `subject`, `content` or `delay`. The step is found by its `id` anywhere in the sequence, including inside conditional branches, and the whole sequence is validated again before it's saved. > **Note:** If the sequence changed after it was read, the request answers `409 SEQUENCE_MODIFIED` instead of overwriting the other change. Retrieve the campaign again and retry. - Required scope: `campaigns:write` ## Request **cURL** ```bash curl -X PATCH "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/sequence/steps/1" \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "updates": { "delay": 3, "subject": "Quick question, {first_name}" } }' ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/sequence/steps/1", { method: "PATCH", headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "updates": { "delay": 3, "subject": "Quick question, {first_name}" } }), }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.patch( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/sequence/steps/1", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, json={ "updates": { "delay": 3, "subject": "Quick question, {first_name}", }, }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. - `step_id` (integer, required): ID of the step. ## Request body - `updates` (object, required): Fields to change on the step. Only message, subject, content and delay may be patched; id, type and branches are structural and require PUT .../sequence. ## Response ### `200` ```json { "message": "string", "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "success": true } ``` - `message` (string, required) - `campaign_id` (string or null, optional) - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 400 | `NO_SEQUENCE` | The campaign has no sequence, so there's no step to update. | | 400 | `SEQUENCE_VALIDATION_FAILED` | The sequence breaks a structural rule, such as an unknown step type or a `linkedin_connection` step without a `conditional` after it. The message lists every rule that failed. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `STEP_NOT_FOUND` | No step with this ID exists in the campaign's sequence, including inside conditional branches. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 409 | `STEP_ID_AMBIGUOUS` | More than one step in the sequence has this ID, so the update can't tell which one to change. | | 409 | `SEQUENCE_MODIFIED` | The sequence changed after it was read. Retrieve the campaign again and retry. | | 413 | `PAYLOAD_TOO_LARGE` | The request body is larger than 1 MB. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/get-campaign-analysis # Get campaign analytics `GET https://api.versionseven.ai/v1/campaigns/{campaign_id}/analysis` Aggregate counts for a campaign: lead states, funnel counts and rates, reply sentiment, per-channel email and LinkedIn stats, and a breakdown per A/B variation. There's no daily timeline and no individual replies. Rates are `null` when their denominator is zero. - Required scope: `campaigns:read` ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/analysis" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/analysis", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/analysis", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. ## Query parameters - `date_filter` (string or null, optional, at most 64 characters, example `"30d"`): Count from this point onwards: a number of days such as `7d`, `30d` or `90d`, or an ISO 8601 timestamp. Answers `400 INVALID_DATE_FILTER` if it can't be parsed. - `variation` (string or null, optional, at most 32 characters, example `"a"`, one of `"a"`, `"b"`, `"unassigned"`, `"all"`): Limit the counts to one A/B variation. Answers `400 INVALID_VARIATION` for any other value. ## Response ### `200` ```json { "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "analysis": { "by_variation": {}, "channels": { "email": { "open_rate_pct": 1, "opened": 1, "positive_leads": 1, "replied_leads": 1, "sent": 1 }, "linkedin": { "accept_rate_pct": 1, "accepted": 1, "connected_leads": 1, "messages": 1, "positive_leads": 1, "replied_leads": 1, "requests": 1 } }, "funnel": { "contacted": 1, "enrolled": 1, "meeting_rate_pct": 1, "meetings": 1, "positive": 1, "positive_rate_pct": 1, "replied": 1, "reply_rate_pct": 1 }, "leads": { "active": 1, "completed": 1, "enrolled": 1, "not_contacted": 1, "paused": 1 }, "replies": { "negative": 1, "neutral": 1, "ooo": 1, "positive": 1, "total": 1 }, "window": { "from": "2026-01-14T19:30:00+00:00", "to": "2026-01-14T19:30:00+00:00", "variation": "a" } }, "success": true } ``` - `campaign_id` (string, required) - `analysis` (object or null, optional): Aggregate analytics for one campaign, counted by each lead's _first_ event of a kind inside the window. - `by_variation` (object or null, optional): Per-arm breakdown keyed by A/B arm: 'a', 'b', 'unassigned' (leads with no arm yet) - `channels` (object or null, optional) - `email` (object or null, optional) - `open_rate_pct` (number or null, optional) - `opened` (integer or null, optional) - `positive_leads` (integer or null, optional) - `replied_leads` (integer or null, optional) - `sent` (integer or null, optional) - `linkedin` (object or null, optional) - `accept_rate_pct` (number or null, optional) - `accepted` (integer or null, optional) - `connected_leads` (integer or null, optional) - `messages` (integer or null, optional) - `positive_leads` (integer or null, optional) - `replied_leads` (integer or null, optional) - `requests` (integer or null, optional): Connection requests sent - `funnel` (object or null, optional) - `contacted` (integer or null, optional) - `enrolled` (integer or null, optional) - `meeting_rate_pct` (number or null, optional): meetings / positive; null when there were no positives - `meetings` (integer or null, optional) - `positive` (integer or null, optional) - `positive_rate_pct` (number or null, optional): positive / contacted; null when nobody was contacted - `replied` (integer or null, optional) - `reply_rate_pct` (number or null, optional): replied / contacted; null when nobody was contacted - `leads` (object or null, optional) - `active` (integer or null, optional) - `completed` (integer or null, optional) - `enrolled` (integer or null, optional) - `not_contacted` (integer or null, optional) - `paused` (integer or null, optional) - `replies` (object or null, optional) - `negative` (integer or null, optional) - `neutral` (integer or null, optional) - `ooo` (integer or null, optional): Out-of-office replies - `positive` (integer or null, optional) - `total` (integer or null, optional) - `window` (object or null, optional) - `from` (string · date-time or null, optional): Start of the window; null = all time - `to` (string · date-time or null, optional): End of the window; null = now - `variation` (string or null, optional, one of `"a"`, `"b"`, `"unassigned"`, `"all"`, `null`): The A/B arm the report covers: 'a', 'b', 'unassigned', or 'all' - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 400 | `INVALID_DATE_FILTER` | `date_filter` must be a number of days such as `30d`, or an ISO 8601 timestamp. | | 400 | `INVALID_VARIATION` | `variation` must be one of `a`, `b`, `unassigned` or `all`. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/get-campaign-queue # Get a campaign's queue `GET https://api.versionseven.ai/v1/campaigns/{campaign_id}/queue` Queue depth and runway for a campaign: the not-started backlog, in-progress and completed leads, first-touch daily capacity, the number of active senders, the most recent lead refill, and `days_of_runway`, the backlog divided by daily capacity. - Required scope: `campaigns:read` ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/queue" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/queue", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/queue", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. ## Response ### `200` ```json { "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "queue": { "active_senders": 0, "backlog": 0, "capacity_source": "none", "completed": 0, "daily_capacity": 0, "days_of_runway": 1, "enabled_accounts": [ "string" ], "has_filters": false, "in_progress": 0, "is_active": false, "last_refill": {}, "limits": {}, "name": "string", "refill_policy": {}, "total": 0, "warnings": [ "string" ] }, "active_senders": 0, "backlog": 0, "capacity_source": "none", "completed": 0, "daily_capacity": 0, "days_of_runway": 1, "enabled_accounts": [ "string" ], "has_filters": false, "in_progress": 0, "is_active": false, "last_refill": {}, "limits": {}, "name": "string", "refill_policy": {}, "success": true, "total": 0, "warnings": [ "string" ] } ``` - `campaign_id` (string, required) - `queue` (object, required): The queue report. Prefer this over the repeated top-level fields. - `active_senders` (integer, optional, default `0`) - `backlog` (integer, optional, default `0`): Sequence leads not yet started (is\_active=false, completed=false) - `capacity_source` (string, optional, default `"none"`, one of `"campaign"`, `"org"`, `"none"`) - `completed` (integer, optional, default `0`) - `daily_capacity` (integer, optional, default `0`): Sum of first-touch daily limits (connection\_request + email) - `days_of_runway` (number or null, optional): backlog / daily\_capacity; null when capacity or senders are zero - `enabled_accounts` (array of strings, optional) - `has_filters` (boolean, optional, default `false`): Whether lead\_database\_filters is set (required for auto-refill) - `in_progress` (integer, optional, default `0`): Sequence leads mid-sequence - `is_active` (boolean, optional, default `false`) - `last_refill` (object or null, optional): The most recent lead-refill job for the campaign (status, counts, timestamps), or null if none has run - `limits` (object, optional): Per-platform {daily\_limit, current\_usage, reset\_date} - `name` (string or null, optional) - `refill_policy` (object or null, optional) - `total` (integer, optional, default `0`) - `warnings` (array of strings, optional) - `active_senders` (integer, optional, default `0`) - `backlog` (integer, optional, default `0`): Sequence leads not yet started (is\_active=false, completed=false) - `capacity_source` (string, optional, default `"none"`, one of `"campaign"`, `"org"`, `"none"`) - `completed` (integer, optional, default `0`) - `daily_capacity` (integer, optional, default `0`): Sum of first-touch daily limits (connection\_request + email) - `days_of_runway` (number or null, optional): backlog / daily\_capacity; null when capacity or senders are zero - `enabled_accounts` (array of strings, optional) - `has_filters` (boolean, optional, default `false`): Whether lead\_database\_filters is set (required for auto-refill) - `in_progress` (integer, optional, default `0`): Sequence leads mid-sequence - `is_active` (boolean, optional, default `false`) - `last_refill` (object or null, optional): The most recent lead-refill job for the campaign (status, counts, timestamps), or null if none has run - `limits` (object, optional): Per-platform {daily\_limit, current\_usage, reset\_date} - `name` (string or null, optional) - `refill_policy` (object or null, optional) - `success` (boolean, optional, default `true`) - `total` (integer, optional, default `0`) - `warnings` (array of strings, optional) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/get-campaign-preflight # Get the activation preflight `GET https://api.versionseven.ai/v1/campaigns/{campaign_id}/preflight` The readiness verdict that activating with [Update a campaign](https://docs.versionseven.ai/api-reference/campaigns/update-campaign) enforces, without activating: the sequence and its step content, variables and lead data coverage, the assigned senders and whether they're connected, sender email authentication, account conflicts and limits, enrolled leads, and the subscription. `ok` is `true` when nothing blocks. Each entry in `checks` carries an `id`, a `label`, a `status` of `pass`, `warn` or `fail`, and a `detail`; `blocking_count` and `warning_count` total them. `issues` lists each problem with a `code`, a `severity`, a `message` and a suggested `fix`. > **Note:** Useful on a live campaign too: a failing check there says why it has stopped sending. - Required scope: `campaigns:read` ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/preflight" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/preflight", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/preflight", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. ## Response ### `200` ```json { "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "ok": true, "blocking_count": 0, "checks": [ {} ], "issues": [ {} ], "success": true, "warning_count": 0 } ``` - `campaign_id` (string, required) - `ok` (boolean, required): True when nothing blocks activation - `blocking_count` (integer, optional, default `0`) - `checks` (array of objects, optional): Every check: {id, label, status: pass|warn|fail, detail, tab, route} - `issues` (array of objects, optional): Machine-readable problems: {code, severity, message, fix} - `success` (boolean, optional, default `true`) - `warning_count` (integer, optional, default `0`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/assign-senders # Assign sender accounts `PUT https://api.versionseven.ai/v1/campaigns/{campaign_id}/senders` Sets the accounts that send for the campaign, replacing the current set. `account_ids` are the `id` values from [List sender accounts](https://docs.versionseven.ai/api-reference/accounts/list-accounts); send at least one. Every account must belong to your organization (`400 INVALID_ACCOUNT` otherwise), be connected (`409 ACCOUNT_NOT_CONNECTED`, with the accounts in `details.account_ids`), and not be sending for another active campaign (`409 ACCOUNT_IN_USE`, with each account and the campaign holding it in `details.conflicts`). The response lists the assigned accounts under `enabled_accounts`, each with its `id`, `platform` and `label`. - Required scope: `campaigns:write` ## Request **cURL** ```bash curl -X PUT "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/senders" \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_ids": [ "0f2f5b9e-8d3a-4c6b-9a2e-1c5d8e7f9a0b" ] }' ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/senders", { method: "PUT", headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "account_ids": [ "0f2f5b9e-8d3a-4c6b-9a2e-1c5d8e7f9a0b" ] }), }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.put( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/senders", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, json={ "account_ids": [ "0f2f5b9e-8d3a-4c6b-9a2e-1c5d8e7f9a0b", ], }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. ## Request body - `account_ids` (array of strings, required): Connected account ids (the `id` from GET /v1/accounts). Replaces the current set. Each must be connected and not sending for another active campaign. ## Response ### `200` ```json { "message": "string", "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "enabled_accounts": [ {} ], "success": true } ``` - `message` (string, required) - `campaign_id` (string, required) - `enabled_accounts` (array of objects, optional) - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 400 | `INVALID_ACCOUNT` | A sender account ID in the request isn't a connected account in your organization. Answered when a connected assistant assigns sender accounts to a campaign. This is a `400` rather than a `404` for the same reason as `INVALID_LEAD`. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 409 | `ACCOUNT_NOT_CONNECTED` | A sender account being assigned to the campaign has disconnected. Reconnect it in the Victoria AI app or with a [reconnect link](https://docs.versionseven.ai/api-reference/accounts/create-connect-link) first. `details.account_ids` lists the accounts. | | 409 | `ACCOUNT_IN_USE` | A sender account sends for one active campaign at a time, and one being assigned is already used by another active campaign. `details.conflicts` names each account and the campaign holding it. | | 413 | `PAYLOAD_TOO_LARGE` | The request body is larger than 1 MB. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/list-ai-fields # List AI fields `GET https://api.versionseven.ai/v1/campaigns/{campaign_id}/ai-fields` The campaign's AI fields, oldest first, active and inactive. An AI field is a variable written for each lead before its first message, from the lead's LinkedIn profile or company website, and used as `{field_name}` in step copy. Only fields with `is_active: true` are generated. - Required scope: `campaigns:read` ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/ai-fields" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/ai-fields", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/ai-fields", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. ## Response ### `200` ```json { "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "ai_fields": [ { "id": "a5614527-0ce6-43be-bd1d-d012b7394867", "field_name": "string", "ai_instructions": "string", "created_at": "2026-01-14T19:30:00+00:00", "data_sources": [ "string" ], "fallback_value": "string", "field_description": "string", "is_active": true, "updated_at": "2026-01-14T19:30:00+00:00" } ], "count": 0, "success": true } ``` - `campaign_id` (string, required) - `ai_fields` (array of objects, optional) - `id` (string, required) - `field_name` (string, required): The variable's name, used as `{field_name}` in step copy. - `ai_instructions` (string, required): What to write for each lead, and from what. - `created_at` (string · date-time or null, optional) - `data_sources` (array of strings or null, optional): Where each lead is researched: `linkedin`, `website` or both. - `fallback_value` (string or null, optional): What a lead gets when personalization fails, so its message still reads well. - `field_description` (string or null, optional) - `is_active` (boolean, optional, default `true`): Only active fields are generated. Set `false` to stop using a field; there's no delete. - `updated_at` (string · date-time or null, optional) - `count` (integer, optional, default `0`) - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/create-ai-field # Create an AI field `POST https://api.versionseven.ai/v1/campaigns/{campaign_id}/ai-fields` Adds an AI field to the campaign. `field_name` starts with a letter and uses only letters, digits and underscores, up to 40 characters; it can't be a name the sender fills from the lead record (`first_name`, `last_name`, `company`, `title`, `employee_count`, `industry`, `linkedin_from_name`, `email_from_name`). Names are compared without regard to case, and an active field with the same name on the campaign answers `409 AI_FIELD_EXISTS`. `ai_instructions` say what to write for each lead. `fallback_value` is required: it's what a lead gets when personalization fails, so a message still reads well. `data_sources` is `linkedin`, `website` or both, and defaults to both. > **Note:** Generating a field for each lead spends your organization's credits when the campaign sends. [Preview the campaign](https://docs.versionseven.ai/api-reference/campaigns/preview-campaign) renders every AI field at its fallback value, which costs nothing. - Required scope: `campaigns:write` ## Request **cURL** ```bash curl -X POST "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/ai-fields" \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "field_name": "recent_news", "field_description": "One recent, specific thing the company did", "ai_instructions": "In one short sentence, name something specific the company announced or shipped in the last few months, from its website. No praise, no adjectives.", "fallback_value": "the work your team is doing", "data_sources": [ "website" ] }' ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/ai-fields", { method: "POST", headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "field_name": "recent_news", "field_description": "One recent, specific thing the company did", "ai_instructions": "In one short sentence, name something specific the company announced or shipped in the last few months, from its website. No praise, no adjectives.", "fallback_value": "the work your team is doing", "data_sources": [ "website" ] }), }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.post( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/ai-fields", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, json={ "field_name": "recent_news", "field_description": "One recent, specific thing the company did", "ai_instructions": "In one short sentence, name something specific the company announced or shipped in the last few months, from its website. No praise, no adjectives.", "fallback_value": "the work your team is doing", "data_sources": [ "website", ], }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. ## Request body - `field_name` (string, required, at most 40 characters): Used as {field\_name} in messages. Letters, digits, underscores. - `ai_instructions` (string, required, at most 4,000 characters): What to write for each lead, and from what - `fallback_value` (string, required, at most 300 characters): Used when personalisation fails, so a lead still gets a sensible message - `data_sources` (array of strings or null, optional): Where to research each lead. Defaults to both. - `field_description` (string or null, optional, at most 500 characters) ## Response ### `201` ```json { "message": "string", "ai_field": { "id": "a5614527-0ce6-43be-bd1d-d012b7394867", "field_name": "string", "ai_instructions": "string", "created_at": "2026-01-14T19:30:00+00:00", "data_sources": [ "string" ], "fallback_value": "string", "field_description": "string", "is_active": true, "updated_at": "2026-01-14T19:30:00+00:00" }, "success": true } ``` - `message` (string, required) - `ai_field` (object or null, optional): A per-lead variable a model writes before each message, e.g. {recent\_post}. - `id` (string, required) - `field_name` (string, required): The variable's name, used as `{field_name}` in step copy. - `ai_instructions` (string, required): What to write for each lead, and from what. - `created_at` (string · date-time or null, optional) - `data_sources` (array of strings or null, optional): Where each lead is researched: `linkedin`, `website` or both. - `fallback_value` (string or null, optional): What a lead gets when personalization fails, so its message still reads well. - `field_description` (string or null, optional) - `is_active` (boolean, optional, default `true`): Only active fields are generated. Set `false` to stop using a field; there's no delete. - `updated_at` (string · date-time or null, optional) - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 409 | `AI_FIELD_EXISTS` | An active AI field with this name already exists on the campaign. Names are compared without regard to case. | | 413 | `PAYLOAD_TOO_LARGE` | The request body is larger than 1 MB. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/update-ai-field # Update an AI field `PATCH https://api.versionseven.ai/v1/campaigns/{campaign_id}/ai-fields/{field_id}` Updates the fields you send and leaves the rest unchanged. `field_name` can't be changed; create a new field instead. `ai_instructions` and `fallback_value` can't be set blank. Set `is_active: false` to stop generating the field for new leads; there's no delete. - Required scope: `campaigns:write` ## Request **cURL** ```bash curl -X PATCH "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/ai-fields/6a28cb94-100b-4c09-8190-4a7b9a0df19d" \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "fallback_value": "the work your team is doing", "is_active": true }' ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/ai-fields/6a28cb94-100b-4c09-8190-4a7b9a0df19d", { method: "PATCH", headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "fallback_value": "the work your team is doing", "is_active": true }), }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.patch( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/ai-fields/6a28cb94-100b-4c09-8190-4a7b9a0df19d", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, json={ "fallback_value": "the work your team is doing", "is_active": True, }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. - `field_id` (string · uuid, required): ID of the field. ## Request body - `ai_instructions` (string or null, optional, at most 4,000 characters) - `data_sources` (array of strings or null, optional) - `fallback_value` (string or null, optional, at most 300 characters) - `field_description` (string or null, optional, at most 500 characters) - `is_active` (boolean or null, optional) ## Response ### `200` ```json { "message": "string", "ai_field": { "id": "a5614527-0ce6-43be-bd1d-d012b7394867", "field_name": "string", "ai_instructions": "string", "created_at": "2026-01-14T19:30:00+00:00", "data_sources": [ "string" ], "fallback_value": "string", "field_description": "string", "is_active": true, "updated_at": "2026-01-14T19:30:00+00:00" }, "success": true } ``` - `message` (string, required) - `ai_field` (object or null, optional): A per-lead variable a model writes before each message, e.g. {recent\_post}. - `id` (string, required) - `field_name` (string, required): The variable's name, used as `{field_name}` in step copy. - `ai_instructions` (string, required): What to write for each lead, and from what. - `created_at` (string · date-time or null, optional) - `data_sources` (array of strings or null, optional): Where each lead is researched: `linkedin`, `website` or both. - `fallback_value` (string or null, optional): What a lead gets when personalization fails, so its message still reads well. - `field_description` (string or null, optional) - `is_active` (boolean, optional, default `true`): Only active fields are generated. Set `false` to stop using a field; there's no delete. - `updated_at` (string · date-time or null, optional) - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 400 | `NO_UPDATES` | The update request contained no fields to change. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `AI_FIELD_NOT_FOUND` | No AI field with this ID exists on the campaign. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 413 | `PAYLOAD_TOO_LARGE` | The request body is larger than 1 MB. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/get-responder # Get the reply agent `GET https://api.versionseven.ai/v1/campaigns/{campaign_id}/responder` The campaign's reply agent, the AI Appointment Setter: whether it answers replies (`is_enabled`), where it sends an interested prospect (`goal_link`), the links it may share (`assets`), its tone and instructions. `responder` is `null` when the campaign has never been configured; create the configuration with [Update the reply agent](https://docs.versionseven.ai/api-reference/campaigns/update-responder). - Required scope: `campaigns:read` ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/responder" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/responder", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/responder", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. ## Response ### `200` ```json { "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "message": "string", "responder": { "agent_name": "string", "agent_version": 1, "assets": [ {} ], "campaign_description": "string", "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "company_name": "string", "custom_instructions": "string", "first_message_mode": "string", "goal": "string", "goal_link": "string", "id": "a5614527-0ce6-43be-bd1d-d012b7394867", "is_enabled": true, "max_replies": 1, "mode": "string", "tone": "string", "updated_at": "2026-01-14T19:30:00+00:00" }, "success": true } ``` - `campaign_id` (string or null, optional) - `message` (string or null, optional) - `responder` (object or null, optional) - `agent_name` (string or null, optional) - `agent_version` (integer or null, optional): `2` is the current agent. Configurations created through the API are always version 2. - `assets` (array of objects or null, optional): Links the agent may share, each with a `link` and a `description` of when to share it. - `campaign_description` (string or null, optional) - `campaign_id` (string or null, optional) - `company_name` (string or null, optional) - `custom_instructions` (string or null, optional) - `first_message_mode` (string or null, optional) - `goal` (string or null, optional) - `goal_link` (string or null, optional): Where the agent sends an interested prospect, such as a booking page. - `id` (string or null, optional) - `is_enabled` (boolean or null, optional): Whether the agent answers replies. Mirrors the campaign's `responder_enabled`. - `max_replies` (integer or null, optional) - `mode` (string or null, optional) - `tone` (string or null, optional): `friendly`, `direct` or `formal`. - `updated_at` (string · date-time or null, optional) - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/update-responder # Update the reply agent `PATCH https://api.versionseven.ai/v1/campaigns/{campaign_id}/responder` Updates the fields you send and leaves the rest unchanged. When the campaign has no configuration yet, one is created for the current agent, answering on its own, with `is_enabled: false` unless you send it. `goal_link` must be an `https` URL. Each entry in `assets` is a `link` (`https`) with a `description` of when to share it. `tone` is `friendly`, `direct` or `formal`. `campaign_description`, `custom_instructions`, `company_name` and `agent_name` are limited to 8,000 characters each. > **Note:** `is_enabled` also sets the campaign's `responder_enabled` flag, the one [Update a campaign](https://docs.versionseven.ai/api-reference/campaigns/update-campaign) changes, so the two never disagree. - Required scope: `campaigns:write` ## Request **cURL** ```bash curl -X PATCH "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/responder" \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "is_enabled": true, "goal_link": "https://calendly.com/acme/intro", "tone": "direct", "custom_instructions": "Offer a 20-minute intro call. If they ask for pricing, say it starts at $99 a month and offer the call for details.", "assets": [ { "link": "https://www.example.com/walkthrough", "description": "A 3-minute product walkthrough, for anyone who asks how it works" } ] }' ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/responder", { method: "PATCH", headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "is_enabled": true, "goal_link": "https://calendly.com/acme/intro", "tone": "direct", "custom_instructions": "Offer a 20-minute intro call. If they ask for pricing, say it starts at $99 a month and offer the call for details.", "assets": [ { "link": "https://www.example.com/walkthrough", "description": "A 3-minute product walkthrough, for anyone who asks how it works" } ] }), }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.patch( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/responder", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, json={ "is_enabled": True, "goal_link": "https://calendly.com/acme/intro", "tone": "direct", "custom_instructions": "Offer a 20-minute intro call. If they ask for pricing, say it starts at $99 a month and offer the call for details.", "assets": [ { "link": "https://www.example.com/walkthrough", "description": "A 3-minute product walkthrough, for anyone who asks how it works", }, ], }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. ## Request body - `agent_name` (string or null, optional, at most 100 characters) - `assets` (array of objects or null, optional): Links the agent may share, e.g. a walkthrough video - `description` (string, required, at most 300 characters): When to share the link, in a sentence the agent can act on. - `link` (string, required, at most 2,000 characters): https\:// URL - `campaign_description` (string or null, optional, at most 8,000 characters) - `company_name` (string or null, optional, at most 200 characters) - `custom_instructions` (string or null, optional, at most 8,000 characters) - `goal_link` (string or null, optional, at most 2,000 characters): Where the agent sends an interested prospect, e.g. a booking page - `is_enabled` (boolean or null, optional): Answer replies automatically - `tone` (string or null, optional, one of `"friendly"`, `"direct"`, `"formal"`) ## Response ### `200` ```json { "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "message": "string", "responder": { "agent_name": "string", "agent_version": 1, "assets": [ {} ], "campaign_description": "string", "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "company_name": "string", "custom_instructions": "string", "first_message_mode": "string", "goal": "string", "goal_link": "string", "id": "a5614527-0ce6-43be-bd1d-d012b7394867", "is_enabled": true, "max_replies": 1, "mode": "string", "tone": "string", "updated_at": "2026-01-14T19:30:00+00:00" }, "success": true } ``` - `campaign_id` (string or null, optional) - `message` (string or null, optional) - `responder` (object or null, optional) - `agent_name` (string or null, optional) - `agent_version` (integer or null, optional): `2` is the current agent. Configurations created through the API are always version 2. - `assets` (array of objects or null, optional): Links the agent may share, each with a `link` and a `description` of when to share it. - `campaign_description` (string or null, optional) - `campaign_id` (string or null, optional) - `company_name` (string or null, optional) - `custom_instructions` (string or null, optional) - `first_message_mode` (string or null, optional) - `goal` (string or null, optional) - `goal_link` (string or null, optional): Where the agent sends an interested prospect, such as a booking page. - `id` (string or null, optional) - `is_enabled` (boolean or null, optional): Whether the agent answers replies. Mirrors the campaign's `responder_enabled`. - `max_replies` (integer or null, optional) - `mode` (string or null, optional) - `tone` (string or null, optional): `friendly`, `direct` or `formal`. - `updated_at` (string · date-time or null, optional) - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 400 | `NO_UPDATES` | The update request contained no fields to change. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 413 | `PAYLOAD_TOO_LARGE` | The request body is larger than 1 MB. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/preview-campaign # Preview a campaign's messages `POST https://api.versionseven.ai/v1/campaigns/{campaign_id}/preview` Renders every step of `variation_a` for a few leads enrolled in the campaign, the way the sender renders them. AI fields are shown at their fallback value, the worst case, so no credits are spent; their names are listed in `ai_fields_at_fallback`. Send `lead_ids` to choose the leads (up to 10), or leave it out for the first `limit` enrolled leads (default 3, at most 10). When the campaign has no enrolled leads, one preview is rendered with placeholder values and `lead` is `null`. Each preview carries the `lead` and its `steps`. A step reports its position (`step`, such as `2.yes.1` for the first step of a conditional's yes branch), `type`, `delay`, rendered `subject` and `body`, the body's length in `chars`, `over_limit` when a connection note is over LinkedIn's 300 characters, and an `error` naming any variable the sender couldn't fill. `view_profile` steps are left out. - Required scope: `campaigns:read` ## Request **cURL** ```bash curl -X POST "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/preview" \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "limit": 2 }' ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/preview", { method: "POST", headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "limit": 2 }), }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.post( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/preview", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, json={ "limit": 2, }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. ## Request body - `lead_ids` (array of strings or null, optional): Leads in the campaign; defaults to the first few - `limit` (integer, optional, ≥ 1, ≤ 10, default `3`) ## Response ### `200` ```json { "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "ai_fields_at_fallback": [ "string" ], "previews": [ {} ], "success": true } ``` - `campaign_id` (string, required) - `ai_fields_at_fallback` (array of strings, optional) - `previews` (array of objects, optional) - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 413 | `PAYLOAD_TOO_LARGE` | The request body is larger than 1 MB. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/list-bookings # List meetings booked `GET https://api.versionseven.ai/v1/campaigns/{campaign_id}/bookings` Meetings booked from the campaign, newest first, up to 500 of each kind. A booking with `kind: "meeting"` is a calendar event, with its `start_time`, `status`, `invitee_name`, `invitee_email`, `canceled_at` and `rescheduled`. A booking with `kind: "confirmation"` is a lead the AI Appointment Setter or a person confirmed as booked; it has no calendar event, so `start_time` is `null` and `booked_at` is when it was confirmed. Both carry the matched `lead` and its `sequence_lead_id`. Confirmations are never verified against a calendar. Send `since` to count only bookings made at or after that time. - Required scope: `campaigns:read` ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/bookings" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/bookings", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/bookings", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. ## Query parameters - `since` (string or null, optional): ISO 8601 timestamp. Only bookings made at or after it are returned. ## Response ### `200` ```json { "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "bookings": [ {} ], "count": 0, "success": true } ``` - `campaign_id` (string, required) - `bookings` (array of objects, optional) - `count` (integer, optional, default `0`) - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/set-ab-testing # Turn A/B testing on or off `POST https://api.versionseven.ai/v1/campaigns/{campaign_id}/ab-testing` Turns the campaign's A/B test on or off. `traffic_split` is the percentage of new leads that get `variation_a`, from 0 to 100; the rest get `variation_b`. It applies to leads enrolled from now on: leads already enrolled keep their arm. Turning the test on needs a `variation_b` with steps, written with [Replace a campaign's sequence](https://docs.versionseven.ai/api-reference/campaigns/replace-sequence); without one, the request answers `409 VARIATION_B_MISSING`. With `enabled: false`, every new lead gets variation A. - Required scope: `campaigns:write` ## Request **cURL** ```bash curl -X POST "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/ab-testing" \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "enabled": true, "traffic_split": 50 }' ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/ab-testing", { method: "POST", headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "enabled": true, "traffic_split": 50 }), }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.post( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/ab-testing", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, json={ "enabled": True, "traffic_split": 50, }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. ## Request body - `enabled` (boolean, required): true runs the test; false sends every new lead variation A - `traffic_split` (integer, optional, ≥ 0, ≤ 100, default `50`): Percentage of NEW leads that get variation A (the rest get B) ## Response ### `200` ```json { "message": "string", "ab_testing_enabled": true, "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "success": true, "traffic_split": 1, "winner": "string" } ``` - `message` (string, required) - `ab_testing_enabled` (boolean or null, optional) - `campaign_id` (string or null, optional) - `success` (boolean, optional, default `true`) - `traffic_split` (integer or null, optional) - `winner` (string or null, optional) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 409 | `VARIATION_B_MISSING` | The campaign's sequence has no `variation_b` with steps, so there's nothing to test against variation A. Write one with [Replace a campaign's sequence](https://docs.versionseven.ai/api-reference/campaigns/replace-sequence) before turning A/B testing on or promoting a winner. | | 413 | `PAYLOAD_TOO_LARGE` | The request body is larger than 1 MB. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/promote-ab-winner # Promote an A/B winner `POST https://api.versionseven.ai/v1/campaigns/{campaign_id}/ab-testing/promote` Ends the test in one arm's favour. With `winner: "a"`, every new lead gets variation A and nothing else moves. With `winner: "b"`, B's steps are copied over variation A with fresh step ids, and every new lead gets them; leads still in flight on A continue on B's remaining steps. Neither arm is deleted and A/B testing stays enabled, so leads already on B finish B either way. The request needs a running test: `409 AB_TESTING_DISABLED` when the test is off, `409 VARIATION_B_MISSING` when the campaign has no variation B. Promoting B rewrites the sequence, so a campaign changed since it was read answers `409 SEQUENCE_MODIFIED`; retry. - Required scope: `campaigns:write` ## Request **cURL** ```bash curl -X POST "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/ab-testing/promote" \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "winner": "b" }' ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/ab-testing/promote", { method: "POST", headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "winner": "b" }), }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.post( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/ab-testing/promote", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, json={ "winner": "b", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. ## Request body - `winner` (string, required, one of `"a"`, `"b"`): a: every new lead gets A. b: B's steps are copied over A and every new lead gets them; leads in flight on A continue on B's remaining steps. Leads already on B finish B either way. ## Response ### `200` ```json { "message": "string", "ab_testing_enabled": true, "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "success": true, "traffic_split": 1, "winner": "string" } ``` - `message` (string, required) - `ab_testing_enabled` (boolean or null, optional) - `campaign_id` (string or null, optional) - `success` (boolean, optional, default `true`) - `traffic_split` (integer or null, optional) - `winner` (string or null, optional) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 400 | `SEQUENCE_VALIDATION_FAILED` | The sequence breaks a structural rule, such as an unknown step type or a `linkedin_connection` step without a `conditional` after it. The message lists every rule that failed. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 409 | `VARIATION_B_MISSING` | The campaign's sequence has no `variation_b` with steps, so there's nothing to test against variation A. Write one with [Replace a campaign's sequence](https://docs.versionseven.ai/api-reference/campaigns/replace-sequence) before turning A/B testing on or promoting a winner. | | 409 | `AB_TESTING_DISABLED` | A/B testing is off for this campaign, so there's no test to promote a winner from. Turn it on with [Turn A/B testing on or off](https://docs.versionseven.ai/api-reference/campaigns/set-ab-testing) first. | | 409 | `SEQUENCE_MODIFIED` | The sequence changed after it was read. Retrieve the campaign again and retry. | | 413 | `PAYLOAD_TOO_LARGE` | The request body is larger than 1 MB. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/get-daily-stats # Get daily stats `GET https://api.versionseven.ai/v1/campaigns/{campaign_id}/daily-stats` Daily activity and outcome counts for a campaign, one row per UTC day. Days with no activity are omitted rather than zero-filled. The rollup is refreshed hourly, so today's row is flagged `partial`. `from` and `to` bound the window (ISO dates; at most 400 days, `from` not after `to`, else `400 INVALID_WINDOW`). `group_by` splits each day by `sender` or `variation`; the default `none` returns one row per day (`400 INVALID_GROUP_BY` otherwise). - Required scope: `campaigns:read` ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/daily-stats" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/daily-stats", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/daily-stats", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. ## Query parameters - `from` (string or null, optional, at most 64 characters, example `"2026-09-01"`): First day (YYYY-MM-DD, or an ISO-8601 datetime whose UTC date is used). Default: 30 days before `to` - `to` (string or null, optional, at most 64 characters, example `"2026-09-30"`): Last day, inclusive (YYYY-MM-DD, or an ISO-8601 datetime whose UTC date is used). Default: today, UTC - `group_by` (string or null, optional, at most 32 characters, example `"sender"`, one of `"none"`, `"sender"`, `"variation"`): 'none' (default): one row per day. 'sender': one row per day and sender. 'variation': one row per day and A/B arm ## Response ### `200` ```json { "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "window": { "from": "string", "to": "string", "timezone": "UTC" }, "count": 0, "days": [ { "day": "string", "emails_opened": 1, "emails_sent": 1, "leads_first_contacted": 1, "leads_first_positive": 1, "leads_first_replied": 1, "li_connection_requests": 1, "li_connections_accepted": 1, "li_messages": 1, "meetings_booked": 1, "partial": true, "platform": "string", "replies_negative": 1, "replies_neutral": 1, "replies_ooo": 1, "replies_positive": 1, "sender_key": "string", "variation": "string" } ], "group_by": "none", "success": true } ``` - `campaign_id` (string, required) - `window` (object, required) - `from` (string, required): First day covered, YYYY-MM-DD - `to` (string, required): Last day covered, YYYY-MM-DD - `timezone` (string, optional, default `"UTC"`): Days are UTC calendar days - `count` (integer, optional, default `0`) - `days` (array of objects, optional) - `day` (string or null, optional): YYYY-MM-DD, UTC - `emails_opened` (integer or null, optional) - `emails_sent` (integer or null, optional) - `leads_first_contacted` (integer or null, optional): Leads whose first touch was that day - `leads_first_positive` (integer or null, optional): Leads whose first positive reply came that day - `leads_first_replied` (integer or null, optional): Leads whose first reply came that day - `li_connection_requests` (integer or null, optional): LinkedIn connection requests sent - `li_connections_accepted` (integer or null, optional): LinkedIn connection requests accepted - `li_messages` (integer or null, optional): LinkedIn messages sent - `meetings_booked` (integer or null, optional) - `partial` (boolean or null, optional): True for today: the rollup is refreshed hourly, so today's row is still filling in - `platform` (string or null, optional): Only when group\_by=sender: 'email' or 'linkedin' - `replies_negative` (integer or null, optional) - `replies_neutral` (integer or null, optional) - `replies_ooo` (integer or null, optional): Out-of-office replies - `replies_positive` (integer or null, optional) - `sender_key` (string or null, optional): Only when group\_by=sender - `variation` (string or null, optional): Only when group\_by=variation: the A/B arm, or null for leads with none - `group_by` (string, optional, default `"none"`, one of `"none"`, `"sender"`, `"variation"`): The grouping applied - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 400 | `INVALID_WINDOW` | The daily-stats window is unparseable, has from after to, or spans more than 400 days. | | 400 | `INVALID_GROUP_BY` | group\_by must be none, sender or variation. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/get-step-funnel # Get the step funnel `GET https://api.versionseven.ai/v1/campaigns/{campaign_id}/step-funnel` The per-step funnel, annotated with the step at each position. Positions follow the sequence's own numbering; a connection check splits later steps into yes/no lanes. Replies are credited to the last message step before them. `waiting` is current state, not a window count. `date_filter` takes a relative window such as `7d` or `30d`; `variation` restricts the funnel to `a` or `b`. - Required scope: `campaigns:read` ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/step-funnel" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/step-funnel", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/step-funnel", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. ## Query parameters - `date_filter` (string or null, optional, at most 64 characters, example `"30d"`): Relative window ('7d', '30d', '90d') or an ISO-8601 timestamp to count from - `variation` (string or null, optional, at most 32 characters, example `"a"`, one of `"a"`, `"b"`, `"unassigned"`, `"all"`): A/B arm to report on: 'a', 'b', 'unassigned' or 'all' (default) ## Response ### `200` ```json { "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "hidden_main_steps": 0, "note": "string", "steps": [ { "channel": "email", "day": 1, "is_message_step": true, "lane": "main", "position": 1, "positive": 1, "positive_rate_pct": 1, "reached": 1, "replied": 1, "reply_rate_pct": 1, "step_id": null, "subject": "string", "type": "string", "waiting": 1 } ], "success": true, "unattributed_outbound": 0, "unattributed_replies": 0, "variation": "all", "window_from": "string" } ``` - `campaign_id` (string, required) - `hidden_main_steps` (integer, optional, default `0`): Main-lane message steps sharing a position with a branch, and so not shown on their own row - `note` (string or null, optional): How to read the numbers - `steps` (array of objects, optional) - `channel` (string or null, optional, one of `"email"`, `"linkedin"`, `null`): 'email' or 'linkedin' - `day` (integer or null, optional): The day of the sequence the step falls on - `is_message_step` (boolean or null, optional): Whether the step sends something a prospect can reply to - `lane` (string or null, optional, one of `"main"`, `"yes"`, `"no"`): 'main', or 'yes' / 'no' for the branch of a connection check - `position` (integer or null, optional): The orchestrator's step number, counted from 1 - `positive` (integer or null, optional): Positive replies credited to this step - `positive_rate_pct` (number or null, optional): positive / reached; null when nobody reached it - `reached` (integer or null, optional): Leads that reached this step in the window - `replied` (integer or null, optional): Replies credited to this step - `reply_rate_pct` (number or null, optional): replied / reached; null when nobody reached it - `step_id` (integer or number or string or null or null, optional): The sequence step's id at this position, when it could be matched. An integer on current sequences; older sequences may hold a fractional number or a string. - `subject` (string or null, optional): The email subject; only on email steps - `type` (string or null, optional): The step type at this position - `waiting` (integer or null, optional): Leads currently sitting at this step (current state; ignores the window) - `success` (boolean, optional, default `true`) - `unattributed_outbound` (integer, optional, default `0`): Sends that could not be matched to a step - `unattributed_replies` (integer, optional, default `0`): Replies that could not be matched to a step - `variation` (string, optional, default `"all"`): The A/B arm the report covers - `window_from` (string or null, optional): Start of the window (ISO-8601); null = all time ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/get-sender-breakdown # Get the sender breakdown `GET https://api.versionseven.ai/v1/campaigns/{campaign_id}/senders/breakdown` Senders that have since been disconnected still appear, with `is_active: false` and no `account_id`, so a campaign's history stays complete after a sender is removed. `date_filter` takes a relative window such as `7d` or `30d`; `variation` restricts the counts to `a` or `b`. - Required scope: `campaigns:read` ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/senders/breakdown" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/senders/breakdown", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/senders/breakdown", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. ## Query parameters - `date_filter` (string or null, optional, at most 64 characters, example `"30d"`): Relative window ('7d', '30d', '90d') or an ISO-8601 timestamp to count from - `variation` (string or null, optional, at most 32 characters, example `"a"`, one of `"a"`, `"b"`, `"unassigned"`, `"all"`): A/B arm to report on: 'a', 'b', 'unassigned' or 'all' (default) ## Response ### `200` ```json { "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "count": 0, "senders": [ { "acceptance_rate_pct": 1, "accepted": 1, "account_id": "739b093a-e9fb-4013-a24b-a87a4069d38a", "contacted_leads": 1, "is_active": true, "label": "string", "messages_sent": 1, "platform": "string", "positive_leads": 1, "positive_rate_pct": 1, "replied_leads": 1, "reply_rate_pct": 1, "requests": 1, "sender_key": "string" } ], "success": true, "variation": "all", "window_from": "string" } ``` - `campaign_id` (string, required) - `count` (integer, optional, default `0`) - `senders` (array of objects, optional) - `acceptance_rate_pct` (number or null, optional): accepted / requests; null when none were sent - `accepted` (integer or null, optional): LinkedIn connection requests accepted - `account_id` (string or null, optional): The connected account's id, when it is still connected - `contacted_leads` (integer or null, optional) - `is_active` (boolean or null, optional): Whether the account is connected now - `label` (string or null, optional): The sender's display name or address - `messages_sent` (integer or null, optional) - `platform` (string or null, optional): 'email' or 'linkedin' - `positive_leads` (integer or null, optional) - `positive_rate_pct` (number or null, optional): positive\_leads / contacted\_leads; null when nobody was contacted - `replied_leads` (integer or null, optional) - `reply_rate_pct` (number or null, optional): replied\_leads / contacted\_leads; null when nobody was contacted - `requests` (integer or null, optional): LinkedIn connection requests sent - `sender_key` (string or null, optional): Stable key for the sender: the provider account id - `success` (boolean, optional, default `true`) - `variation` (string, optional, default `"all"`): The A/B arm the report covers - `window_from` (string or null, optional): Start of the window (ISO-8601); null = all time ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/get-personalization-quality # Get personalization quality `GET https://api.versionseven.ai/v1/campaigns/{campaign_id}/personalization-quality` One row per [AI personalization field](https://docs.versionseven.ai/guides/ai-personalization-fields) with its runs over the window by outcome (`success`, `fallback`, `generic`, `held`, `error`). `held_rate_pct` over 30 means the field is mostly not producing usable copy; `insufficient_data_rate_pct` over 60 means the data to write it is usually missing, and the field's prompt or the lead data needs attention. `days` sets the window (default 14). - Required scope: `campaigns:read` ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/personalization-quality" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/personalization-quality", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/personalization-quality", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. ## Query parameters - `days` (integer, optional, ≥ 1, ≤ 365, default `14`): How many days back to count, from now ## Response ### `200` ```json { "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "days": 1, "since": "string", "fields": [ { "error": 1, "fallback": 1, "field_name": "string", "generic": 1, "held": 1, "held_rate_pct": 1, "insufficient_data": 1, "insufficient_data_rate_pct": 1, "runs": 1, "success": 1 } ], "success": true, "total_runs": 0 } ``` - `campaign_id` (string, required) - `days` (integer, required) - `since` (string, required): Start of the window (ISO-8601) - `fields` (array of objects, optional) - `error` (integer or null, optional): Runs that failed - `fallback` (integer or null, optional): Runs that used the field's fallback text - `field_name` (string or null, optional) - `generic` (integer or null, optional): Runs whose output was judged generic - `held` (integer or null, optional): Runs held back rather than sent - `held_rate_pct` (number or null, optional): (held + generic + error) / runs: the share of sends with no usable copy; null when there were no runs - `insufficient_data` (integer or null, optional): Runs where the model reported too little to work with - `insufficient_data_rate_pct` (number or null, optional): insufficient\_data / runs; null when there were no runs - `runs` (integer or null, optional): Live sends where the field was resolved - `success` (integer or null, optional): Runs that produced personalised copy - `success` (boolean, optional, default `true`) - `total_runs` (integer, optional, default `0`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/get-ab-cohorts # Compare A/B cohorts `GET https://api.versionseven.ai/v1/campaigns/{campaign_id}/ab-cohorts` Per-arm counts for leads first contacted at or after `since`. Each lead counts once, in the arm it was assigned, with its first reply, first positive reply and first negative reply: `arms.a` and `arms.b` each carry `contacted`, `replied`, `positive` and `negative`. Unlike [Get campaign analytics](https://docs.versionseven.ai/api-reference/campaigns/get-campaign-analysis), every number is over the same set of leads, those who could have seen either arm, so the arms can be compared directly. Pass the time the test was turned on as `since`. - Required scope: `campaigns:read` ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/ab-cohorts?since=string" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/ab-cohorts?since=string", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/ab-cohorts", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, params={ "since": "string", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. ## Query parameters - `since` (string, required): ISO 8601 timestamp the test started. Only leads first contacted at or after it count. Answers `400 INVALID_DATE_FILTER` if it can't be parsed. ## Response ### `200` ```json { "campaign_id": "550e8400-e29b-41d4-a716-446655440000", "since": "string", "arms": {}, "success": true } ``` - `campaign_id` (string, required) - `since` (string, required) - `arms` (object, optional): a / b -> {contacted, replied, positive, negative} - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 400 | `INVALID_DATE_FILTER` | `date_filter` must be a number of days such as `30d`, or an ISO 8601 timestamp. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/list-webhooks # List a campaign's webhooks `GET https://api.versionseven.ai/v1/campaigns/{campaign_id}/webhooks` Every webhook registered on the campaign, enabled and disabled. Signing secrets are never returned. - Required scope: `campaigns:read` ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/webhooks" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/webhooks", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/webhooks", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. ## Query parameters - `limit` (integer, optional, ≥ 1, ≤ 500, default `100`): Maximum rows to return - `offset` (integer, optional, ≥ 0, default `0`): Rows to skip before returning results ## Response ### `200` ```json { "total": 1, "limit": 1, "offset": 1, "has_more": true, "webhooks": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "webhook_url": "https://api.example.com/webhooks/victoria", "is_enabled": true, "created_at": "2024-01-15T10:30:00+00:00", "updated_at": "2024-01-20T14:45:00+00:00" } ], "count": 1, "success": true } ``` - `total` (integer, required): Total rows matching the query, across all pages - `limit` (integer, required): Page size that was applied - `offset` (integer, required): Rows skipped before this page - `has_more` (boolean, required): Whether a further page exists - `webhooks` (array of objects, required): List of webhook subscriptions for the campaign - `id` (string, required, example `"550e8400-e29b-41d4-a716-446655440000"`): Webhook subscription ID - `webhook_url` (string, required, example `"https://api.example.com/webhooks/victoria"`): URL that receives webhook events - `is_enabled` (boolean, required, example `true`): Whether this webhook is currently active - `created_at` (string, required, example `"2024-01-15T10:30:00+00:00"`): When this webhook was created - `updated_at` (string or null, optional, example `"2024-01-20T14:45:00+00:00"`): When this webhook was last updated - `count` (integer, required): Webhooks in this page. Deprecated: use `total` - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/create-webhook # Create a webhook `POST https://api.versionseven.ai/v1/campaigns/{campaign_id}/webhooks` Registers an HTTPS URL to receive this campaign's [`prospect_response`](https://docs.versionseven.ai/api-reference/webhooks/prospect-response) events. Answers `201` when a webhook is created, `200` when a disabled webhook for the same URL is enabled again, and `409 DUPLICATE_WEBHOOK` when that URL is already active. A campaign has one response webhook. When a different URL already holds it, the request answers `409 WEBHOOK_SLOT_TAKEN` unless you send `replace: true`, which repoints that webhook at the new URL (`200`, with `replaced: true`); the previous receiver then stops getting this campaign's events. Send your own `secret` so both sides hold the same signing key, or leave it out and one is generated for you. > **Warning:** A generated `secret` appears in this response and nowhere else. Store it before you discard the response; if you lose it, [set a new one](https://docs.versionseven.ai/api-reference/campaigns/rotate-webhook-secret). - Required scope: `campaigns:write` ## Request **cURL** ```bash curl -X POST "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/webhooks" \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "webhook_url": "https://api.example.com/webhooks/victoria" }' ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/webhooks", { method: "POST", headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "webhook_url": "https://api.example.com/webhooks/victoria" }), }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.post( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/webhooks", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, json={ "webhook_url": "https://api.example.com/webhooks/victoria", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. ## Request body - `webhook_url` (string, required, at most 2,000 characters, example `"https://api.example.com/webhooks/victoria"`): URL to receive webhook events - `replace` (boolean, optional, default `false`): A campaign has one response webhook. When a different URL already holds it, the request is refused with WEBHOOK\_SLOT\_TAKEN unless this is true, in which case that webhook is repointed at webhook\_url and the previous receiver stops getting this campaign's responses. - `secret` (string or null, optional, at least 16 characters, at most 2,000 characters): Caller-supplied HMAC signing secret so the receiver holds the same key; generated server-side when omitted (and then unrecoverable by the receiver) ## Response ### `200 / 201` ```json { "webhook_id": "3a94d596-645d-47ff-a032-c70b299fc8f3", "created": true, "message": "Webhook activated successfully", "replaced": true, "secret": "string", "success": true } ``` - `webhook_id` (string, required): ID of the webhook record - `created` (boolean or null, optional): True when a new webhook was created, false when an existing disabled one was re-enabled - `message` (string, optional, default `"Webhook activated successfully"`) - `replaced` (boolean or null, optional): True when an existing webhook for a different URL was repointed (replace=true) - `secret` (string or null, optional): Present only when the signing secret was generated server-side, and only in this response: store it now, it cannot be retrieved again. Absent when you supplied your own. - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 400 | `INVALID_WEBHOOK_URL` | The webhook URL must use `https` and resolve to a public address. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 409 | `DUPLICATE_WEBHOOK` | An active webhook for this URL already exists on the campaign. `details.webhook_id` identifies it. | | 409 | `WEBHOOK_SLOT_TAKEN` | A campaign has one response webhook, and a different URL already holds this campaign's. `details.webhook_id` identifies it. Send `replace: true` to repoint that webhook at the new URL; the previous receiver then stops getting this campaign's events. | | 413 | `PAYLOAD_TOO_LARGE` | The request body is larger than 1 MB. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/delete-webhook # Delete a webhook `DELETE https://api.versionseven.ai/v1/campaigns/{campaign_id}/webhooks/{webhook_id}` Stops delivering the campaign's events to this webhook. The subscription is disabled rather than destroyed, so registering the same URL again re-enables it, along with its signing secret. - Required scope: `campaigns:write` ## Request **cURL** ```bash curl -X DELETE "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/webhooks/3a94d596-645d-47ff-a032-c70b299fc8f3" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/webhooks/3a94d596-645d-47ff-a032-c70b299fc8f3", { method: "DELETE", headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.delete( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/webhooks/3a94d596-645d-47ff-a032-c70b299fc8f3", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. - `webhook_id` (string · uuid, required): ID of the webhook. ## Response ### `200` ```json { "message": "Webhook deactivated successfully", "success": true } ``` - `message` (string, optional, default `"Webhook deactivated successfully"`) - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `WEBHOOK_NOT_FOUND` | No webhook with this ID exists on the campaign. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/campaigns/rotate-webhook-secret # Set or rotate a webhook secret `POST https://api.versionseven.ai/v1/campaigns/{campaign_id}/webhooks/{webhook_id}/secret` Sets or replaces the secret used to sign this webhook's deliveries. Each delivery then carries `X-Signature-256: sha256=`, an HMAC-SHA256 of the exact request body keyed with the secret. Send your own `secret`, or leave it out and one is generated. Use this to add signing to a webhook created without a secret, or to replace a secret you think is compromised. > **Warning:** A generated `secret` appears in this response and nowhere else. Store it before you discard the response. - Required scope: `campaigns:write` ## Request **cURL** ```bash curl -X POST "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/webhooks/3a94d596-645d-47ff-a032-c70b299fc8f3/secret" \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/webhooks/3a94d596-645d-47ff-a032-c70b299fc8f3/secret", { method: "POST", headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({}), }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.post( "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/webhooks/3a94d596-645d-47ff-a032-c70b299fc8f3/secret", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, json={}, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `campaign_id` (string · uuid, required): ID of the campaign. - `webhook_id` (string · uuid, required): ID of the webhook. ## Request body - `secret` (string or null, optional, at least 16 characters, at most 2,000 characters): Your own signing key, so both sides hold the same value. Omit it and one is generated and returned once. ## Response ### `200` ```json { "webhook_id": "3a94d596-645d-47ff-a032-c70b299fc8f3", "message": "Webhook signing secret rotated", "secret": "string", "success": true } ``` - `webhook_id` (string, required): ID of the webhook whose secret was rotated - `message` (string, optional, default `"Webhook signing secret rotated"`) - `secret` (string or null, optional): Present only when the secret was generated server-side, and only in this response: store it now, it cannot be retrieved again. Absent when you supplied your own. - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. | | 404 | `WEBHOOK_NOT_FOUND` | No webhook with this ID exists on the campaign. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 413 | `PAYLOAD_TOO_LARGE` | The request body is larger than 1 MB. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/crm-pipelines/list-pipelines # List pipelines `GET https://api.versionseven.ai/v1/crm/pipelines` Active pipelines in your organization, oldest first. Stages aren't included; retrieve a pipeline to get them. - Required scope: `crm:read` ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/crm/pipelines" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/crm/pipelines", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/crm/pipelines", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Query parameters - `limit` (integer, optional, ≥ 1, ≤ 500, default `100`): Maximum rows to return - `offset` (integer, optional, ≥ 0, default `0`): Rows to skip before returning results ## Response ### `200` ```json { "total": 1, "limit": 1, "offset": 1, "has_more": true, "message": "string", "pipelines": [ { "name": "Enterprise Sales", "created_at": "2024-01-15T10:30:00+00:00", "id": "c1d2e3f4-a5b6-7890-cdef-123456789012", "is_active": true, "is_default": false, "updated_at": "2024-01-20T14:45:00+00:00" } ], "count": 1, "success": true } ``` - `total` (integer, required): Total rows matching the query, across all pages - `limit` (integer, required): Page size that was applied - `offset` (integer, required): Rows skipped before this page - `has_more` (boolean, required): Whether a further page exists - `message` (string, required) - `pipelines` (array of objects, required): List of pipelines - `name` (string, required, example `"Enterprise Sales"`): Pipeline name - `created_at` (string · date-time or null, optional, example `"2024-01-15T10:30:00+00:00"`): When the pipeline was created - `id` (string or null, optional, example `"c1d2e3f4-a5b6-7890-cdef-123456789012"`): Pipeline ID - `is_active` (boolean, optional, default `true`): Whether this pipeline is active - `is_default` (boolean, optional, default `false`): Whether this is the default pipeline - `updated_at` (string · date-time or null, optional, example `"2024-01-20T14:45:00+00:00"`): When the pipeline was last updated - `count` (integer, required): Pipelines in this page - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/crm-pipelines/retrieve-pipeline # Retrieve a pipeline `GET https://api.versionseven.ai/v1/crm/pipelines/{pipeline_id}` A pipeline with its active stages, in display order. - Required scope: `crm:read` ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/crm/pipelines/a5ad8316-bbdf-4e40-9e27-da8c81593927" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/crm/pipelines/a5ad8316-bbdf-4e40-9e27-da8c81593927", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/crm/pipelines/a5ad8316-bbdf-4e40-9e27-da8c81593927", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `pipeline_id` (string · uuid, required): ID of the pipeline. ## Response ### `200` ```json { "message": "string", "pipeline": { "name": "Enterprise Sales", "created_at": "2024-01-15T10:30:00+00:00", "id": "c1d2e3f4-a5b6-7890-cdef-123456789012", "is_active": true, "is_default": false, "updated_at": "2024-01-20T14:45:00+00:00" }, "stages": [ { "pipeline_id": "c1d2e3f4-a5b6-7890-cdef-123456789012", "name": "Qualified", "display_order": 2, "created_at": "2024-01-15T10:30:00+00:00", "id": "d2e3f4a5-b6c7-8901-def0-234567890123", "is_active": true, "updated_at": "2024-01-20T14:45:00+00:00" } ], "success": true } ``` - `message` (string, required) - `pipeline` (object, required): Pipeline data - `name` (string, required, example `"Enterprise Sales"`): Pipeline name - `created_at` (string · date-time or null, optional, example `"2024-01-15T10:30:00+00:00"`): When the pipeline was created - `id` (string or null, optional, example `"c1d2e3f4-a5b6-7890-cdef-123456789012"`): Pipeline ID - `is_active` (boolean, optional, default `true`): Whether this pipeline is active - `is_default` (boolean, optional, default `false`): Whether this is the default pipeline - `updated_at` (string · date-time or null, optional, example `"2024-01-20T14:45:00+00:00"`): When the pipeline was last updated - `stages` (array of objects, optional): Stages in this pipeline - `pipeline_id` (string, required, example `"c1d2e3f4-a5b6-7890-cdef-123456789012"`): ID of the parent pipeline - `name` (string, required, example `"Qualified"`): Stage name - `display_order` (integer, required, example `2`): Order in which the stage appears (lower = earlier) - `created_at` (string · date-time or null, optional, example `"2024-01-15T10:30:00+00:00"`): When the stage was created - `id` (string or null, optional, example `"d2e3f4a5-b6c7-8901-def0-234567890123"`): Stage ID - `is_active` (boolean, optional, default `true`): Whether this stage is active - `updated_at` (string · date-time or null, optional, example `"2024-01-20T14:45:00+00:00"`): When the stage was last updated - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `PIPELINE_NOT_FOUND` | No pipeline with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/crm-pipelines/update-pipeline # Update a pipeline `PATCH https://api.versionseven.ai/v1/crm/pipelines/{pipeline_id}` Updates the fields you send and leaves the rest unchanged. Setting `is_default` to `true` makes every other pipeline in your organization non-default. - Required scope: `crm:write` ## Request **cURL** ```bash curl -X PATCH "https://api.versionseven.ai/v1/crm/pipelines/a5ad8316-bbdf-4e40-9e27-da8c81593927" \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Enterprise Sales" }' ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/crm/pipelines/a5ad8316-bbdf-4e40-9e27-da8c81593927", { method: "PATCH", headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "name": "Enterprise Sales" }), }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.patch( "https://api.versionseven.ai/v1/crm/pipelines/a5ad8316-bbdf-4e40-9e27-da8c81593927", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, json={ "name": "Enterprise Sales", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `pipeline_id` (string · uuid, required): ID of the pipeline. ## Request body - `is_active` (boolean or null, optional): Whether this pipeline is active (inactive pipelines are hidden) - `is_default` (boolean or null, optional): Set as the default pipeline for new deals - `name` (string or null, optional, at most 2,000 characters, example `"Enterprise Sales"`): Pipeline name ## Response ### `200` ```json { "message": "string", "pipeline": { "name": "Enterprise Sales", "created_at": "2024-01-15T10:30:00+00:00", "id": "c1d2e3f4-a5b6-7890-cdef-123456789012", "is_active": true, "is_default": false, "updated_at": "2024-01-20T14:45:00+00:00" }, "success": true } ``` - `message` (string, required) - `pipeline` (object, required): The updated pipeline - `name` (string, required, example `"Enterprise Sales"`): Pipeline name - `created_at` (string · date-time or null, optional, example `"2024-01-15T10:30:00+00:00"`): When the pipeline was created - `id` (string or null, optional, example `"c1d2e3f4-a5b6-7890-cdef-123456789012"`): Pipeline ID - `is_active` (boolean, optional, default `true`): Whether this pipeline is active - `is_default` (boolean, optional, default `false`): Whether this is the default pipeline - `updated_at` (string · date-time or null, optional, example `"2024-01-20T14:45:00+00:00"`): When the pipeline was last updated - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 400 | `NO_UPDATES` | The update request contained no fields to change. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `PIPELINE_NOT_FOUND` | No pipeline with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 413 | `PAYLOAD_TOO_LARGE` | The request body is larger than 1 MB. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/crm-deals/list-deals # List deals `GET https://api.versionseven.ai/v1/crm/deals` Deals in your organization, newest first, each with a summary of its lead. Filter by `stage_id`, `owner_id` or `lead_id`, and page with `limit` and `offset`. - Required scope: `crm:read` ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/crm/deals" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/crm/deals", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/crm/deals", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Query parameters - `stage_id` (string · uuid or null, optional): Filter by stage ID - `owner_id` (string · uuid or null, optional): Filter by owner ID - `lead_id` (string · uuid or null, optional): Filter by lead ID - `limit` (integer, optional, ≥ 1, ≤ 100, default `50`): Maximum number of deals to return - `offset` (integer, optional, ≥ 0, default `0`): Number of deals to skip ## Response ### `200` ```json { "total": 1, "limit": 1, "offset": 1, "has_more": true, "message": "string", "deals": [ { "name": "Acme Corp - Enterprise License", "actual_close_date": "2026-01-14T19:30:00+00:00", "created_at": "2024-01-15T10:30:00+00:00", "custom_fields": {}, "expected_close_date": "2024-03-31T00:00:00+00:00", "id": "e3f4a5b6-c7d8-9012-ef01-345678901234", "lead": {}, "lead_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "metadata": {}, "notes": "Initial meeting scheduled for next week", "owner_id": "f4a5b6c7-d8e9-0123-f012-456789012345", "probability": 75, "stage_id": "d2e3f4a5-b6c7-8901-def0-234567890123", "updated_at": "2024-01-20T14:45:00+00:00", "value": 50000 } ], "count": 1, "success": true } ``` - `total` (integer, required): Total rows matching the query, across all pages - `limit` (integer, required): Page size that was applied - `offset` (integer, required): Rows skipped before this page - `has_more` (boolean, required): Whether a further page exists - `message` (string, required) - `deals` (array of objects, required): List of deals - `name` (string, required, example `"Acme Corp - Enterprise License"`): Deal name/title - `actual_close_date` (string · date-time or null, optional): Actual close date (set when deal is won/lost) - `created_at` (string · date-time or null, optional, example `"2024-01-15T10:30:00+00:00"`): When the deal was created - `custom_fields` (object or null, optional): Custom fields as key-value pairs - `expected_close_date` (string · date-time or null, optional, example `"2024-03-31T00:00:00+00:00"`): Expected close date - `id` (string or null, optional, example `"e3f4a5b6-c7d8-9012-ef01-345678901234"`): Deal ID - `lead` (object or null, optional): Associated lead data - `lead_id` (string or null, optional, example `"a1b2c3d4-e5f6-7890-abcd-ef1234567890"`): Associated lead ID - `metadata` (object or null, optional): Additional metadata - `notes` (string or null, optional, example `"Initial meeting scheduled for next week"`): Deal notes - `owner_id` (string or null, optional, example `"f4a5b6c7-d8e9-0123-f012-456789012345"`): ID of the deal owner (user) - `probability` (integer or null, optional, example `75`): Win probability percentage (0-100) - `stage_id` (string or null, optional, example `"d2e3f4a5-b6c7-8901-def0-234567890123"`): Current stage ID - `updated_at` (string · date-time or null, optional, example `"2024-01-20T14:45:00+00:00"`): When the deal was last updated - `value` (number or null, optional, example `50000`): Deal value in dollars - `count` (integer, required): Deals in this page - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/crm-deals/create-deal # Create a deal `POST https://api.versionseven.ai/v1/crm/deals` Creates a deal. Attach an existing lead with `lead_id`, or send `lead` to create one with the deal. Send one or the other, not both. An inline `lead` is created the way [Create a lead](https://docs.versionseven.ai/api-reference/leads/create-lead) does it, and answers the same `409` codes when it can't be. > **Note:** A `lead_id`, `stage_id` or `owner_id` that isn't in your organization answers `400`, not `404`, so the response never confirms an ID that belongs to another organization. - Required scope: `crm:write` - Accepts an `Idempotency-Key` header, which makes retries safe. See [Idempotency](https://docs.versionseven.ai/guides/idempotency). ## Request **cURL** ```bash curl -X POST "https://api.versionseven.ai/v1/crm/deals" \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "name": "Acme Corp - Enterprise License", "expected_close_date": "2024-03-31T00:00:00+00:00", "lead_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "metadata": { "campaign": "Q1 outreach", "source": "inbound" }, "notes": "Initial meeting scheduled for next week", "owner_id": "f4a5b6c7-d8e9-0123-f012-456789012345", "probability": 25, "stage_id": "d2e3f4a5-b6c7-8901-def0-234567890123", "value": 50000 }' ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/crm/deals", { method: "POST", headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID(), }, body: JSON.stringify({ "name": "Acme Corp - Enterprise License", "expected_close_date": "2024-03-31T00:00:00+00:00", "lead_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "metadata": { "campaign": "Q1 outreach", "source": "inbound" }, "notes": "Initial meeting scheduled for next week", "owner_id": "f4a5b6c7-d8e9-0123-f012-456789012345", "probability": 25, "stage_id": "d2e3f4a5-b6c7-8901-def0-234567890123", "value": 50000 }), }); const data = await response.json(); ``` **Python** ```python import os import uuid import requests response = requests.post( "https://api.versionseven.ai/v1/crm/deals", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", "Idempotency-Key": str(uuid.uuid4()), }, json={ "name": "Acme Corp - Enterprise License", "expected_close_date": "2024-03-31T00:00:00+00:00", "lead_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "metadata": { "campaign": "Q1 outreach", "source": "inbound", }, "notes": "Initial meeting scheduled for next week", "owner_id": "f4a5b6c7-d8e9-0123-f012-456789012345", "probability": 25, "stage_id": "d2e3f4a5-b6c7-8901-def0-234567890123", "value": 50000, }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. - `Idempotency-Key` (string, optional, at most 255 characters): Any unique string, such as a UUID, that makes retrying this request safe. A repeat with the same key for at least 24 hours returns the original response instead of doing the work again. Use a new key for each distinct request. ## Request body - `name` (string, required, at most 2,000 characters, example `"Acme Corp - Enterprise License"`): Deal name/title - `expected_close_date` (string · date-time or null, optional, at most 2,000 characters, example `"2024-03-31T00:00:00+00:00"`): Expected close date - `lead` (object or null, optional): Inline lead data — a new lead will be created and linked to this deal. Cannot be used together with lead\_id. - `first_name` (string, required, at most 2,000 characters, example `"Sarah"`): Lead's first name - `last_name` (string, required, at most 2,000 characters, example `"Johnson"`): Lead's last name - `annual_revenue` (integer or null, optional, ≥ 0, example `5000000`): Company annual revenue in dollars - `company` (string or null, optional, at most 2,000 characters, example `"Acme Corp"`): Company name - `company_website` (string or null, optional, at most 2,000 characters, example `"https://acmecorp.com"`): Company website URL - `email` (string or null, optional, at most 2,000 characters, example `"sarah.johnson@acmecorp.com"`): Lead's email address (required if linkedin\_url not provided) - `employees` (integer or null, optional, ≥ 0, example `150`): Number of employees - `industry` (string or null, optional, at most 2,000 characters, example `"Technology"`): Industry sector - `linkedin_url` (string or null, optional, at most 2,000 characters, example `"https://linkedin.com/in/sarahjohnson"`): Lead's LinkedIn profile URL (required if email not provided) - `title` (string or null, optional, at most 2,000 characters, example `"VP of Sales"`): Job title - `lead_id` (string · uuid or null, optional, at most 2,000 characters, example `"a1b2c3d4-e5f6-7890-abcd-ef1234567890"`): ID of an existing lead to associate with this deal - `metadata` (object or null, optional): Additional metadata - `notes` (string or null, optional, at most 2,000 characters, example `"Initial meeting scheduled for next week"`): Deal notes - `owner_id` (string · uuid or null, optional, at most 2,000 characters, example `"f4a5b6c7-d8e9-0123-f012-456789012345"`): ID of the deal owner (user) - `probability` (integer or null, optional, ≥ 0, ≤ 100, example `25`): Win probability percentage (0-100) - `stage_id` (string · uuid or null, optional, at most 2,000 characters, example `"d2e3f4a5-b6c7-8901-def0-234567890123"`): Initial stage ID (defaults to first stage in pipeline) - `value` (number or null, optional, ≥ 0, example `50000`): Deal value in dollars ## Response ### `201` ```json { "message": "string", "deal": { "name": "Acme Corp - Enterprise License", "actual_close_date": "2026-01-14T19:30:00+00:00", "created_at": "2024-01-15T10:30:00+00:00", "custom_fields": {}, "expected_close_date": "2024-03-31T00:00:00+00:00", "id": "e3f4a5b6-c7d8-9012-ef01-345678901234", "lead_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "metadata": {}, "notes": "Initial meeting scheduled for next week", "owner_id": "f4a5b6c7-d8e9-0123-f012-456789012345", "probability": 75, "stage_id": "d2e3f4a5-b6c7-8901-def0-234567890123", "updated_at": "2024-01-20T14:45:00+00:00", "value": 50000 }, "success": true } ``` - `message` (string, required) - `deal` (object, required): The created deal - `name` (string, required, example `"Acme Corp - Enterprise License"`): Deal name/title - `actual_close_date` (string · date-time or null, optional): Actual close date (set when deal is won/lost) - `created_at` (string · date-time or null, optional, example `"2024-01-15T10:30:00+00:00"`): When the deal was created - `custom_fields` (object or null, optional): Custom fields as key-value pairs - `expected_close_date` (string · date-time or null, optional, example `"2024-03-31T00:00:00+00:00"`): Expected close date - `id` (string or null, optional, example `"e3f4a5b6-c7d8-9012-ef01-345678901234"`): Deal ID - `lead_id` (string or null, optional, example `"a1b2c3d4-e5f6-7890-abcd-ef1234567890"`): Associated lead ID - `metadata` (object or null, optional): Additional metadata - `notes` (string or null, optional, example `"Initial meeting scheduled for next week"`): Deal notes - `owner_id` (string or null, optional, example `"f4a5b6c7-d8e9-0123-f012-456789012345"`): ID of the deal owner (user) - `probability` (integer or null, optional, example `75`): Win probability percentage (0-100) - `stage_id` (string or null, optional, example `"d2e3f4a5-b6c7-8901-def0-234567890123"`): Current stage ID - `updated_at` (string · date-time or null, optional, example `"2024-01-20T14:45:00+00:00"`): When the deal was last updated - `value` (number or null, optional, example `50000`): Deal value in dollars - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 400 | `INVALID_LEAD` | The `lead_id` in the request body isn't a lead in your organization. This is a `400` rather than a `404` so the response never confirms an ID that belongs to another organization. | | 400 | `INVALID_STAGE` | The `stage_id` in the request body isn't a stage in your organization. This is a `400` rather than a `404` for the same reason as `INVALID_LEAD`. | | 400 | `INVALID_OWNER` | The `owner_id` in the request body isn't a member of your organization. This is a `400` rather than a `404` for the same reason as `INVALID_LEAD`. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `TRIAL_LEAD_CAP_REACHED` | The organization is on a free trial and has used up its lead quota, so the lead wasn't created. `details` carries the `cap`, the number `used` and the `remaining` count. Subscribe in the Victoria AI app to add more. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 409 | `LEAD_ALREADY_EXISTS` | A lead with the same email or LinkedIn URL already exists in your organization. From [Create a lead](https://docs.versionseven.ai/api-reference/leads/create-lead), this means the request named no `campaign_id` to enrol the existing lead in, or raced an identical request. Changing a lead's email or LinkedIn URL to another lead's answers it too. When it's known, `details.existing_lead_id` identifies the existing lead. | | 409 | `LEAD_SUPPRESSED` | The contact is on your organization's do-not-contact list, so the lead wasn't created, enrolled or updated. `details.suppressed_by` is `email`, `domain` or `linkedin_url`, and `details.value` is the entry that matched. | | 409 | `IDEMPOTENCY_KEY_REUSED` | This `Idempotency-Key` was already used with a different request. Use a new key for each distinct request. | | 409 | `IDEMPOTENCY_IN_PROGRESS` | The first request with this `Idempotency-Key` is still running. Retry after the `Retry-After` interval. | | 413 | `PAYLOAD_TOO_LARGE` | The request body is larger than 1 MB. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`, and on `409 IDEMPOTENCY_IN_PROGRESS`: seconds to wait before retrying. | | `Idempotent-Replay` | `true` when the response is a stored replay of an earlier request with the same `Idempotency-Key`. | --- Source: https://docs.versionseven.ai/api-reference/crm-deals/retrieve-deal # Retrieve a deal `GET https://api.versionseven.ai/v1/crm/deals/{deal_id}` A single deal, with the lead it belongs to. - Required scope: `crm:read` ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/crm/deals/a47d043f-fc68-49de-bf05-8861f91f61e8" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/crm/deals/a47d043f-fc68-49de-bf05-8861f91f61e8", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/crm/deals/a47d043f-fc68-49de-bf05-8861f91f61e8", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `deal_id` (string · uuid, required): ID of the deal. ## Response ### `200` ```json { "message": "string", "deal": { "name": "Acme Corp - Enterprise License", "actual_close_date": "2026-01-14T19:30:00+00:00", "created_at": "2024-01-15T10:30:00+00:00", "custom_fields": {}, "expected_close_date": "2024-03-31T00:00:00+00:00", "id": "e3f4a5b6-c7d8-9012-ef01-345678901234", "lead": {}, "lead_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "metadata": {}, "notes": "Initial meeting scheduled for next week", "owner_id": "f4a5b6c7-d8e9-0123-f012-456789012345", "probability": 75, "stage_id": "d2e3f4a5-b6c7-8901-def0-234567890123", "updated_at": "2024-01-20T14:45:00+00:00", "value": 50000 }, "success": true } ``` - `message` (string, required) - `deal` (object, required): Deal data with lead info - `name` (string, required, example `"Acme Corp - Enterprise License"`): Deal name/title - `actual_close_date` (string · date-time or null, optional): Actual close date (set when deal is won/lost) - `created_at` (string · date-time or null, optional, example `"2024-01-15T10:30:00+00:00"`): When the deal was created - `custom_fields` (object or null, optional): Custom fields as key-value pairs - `expected_close_date` (string · date-time or null, optional, example `"2024-03-31T00:00:00+00:00"`): Expected close date - `id` (string or null, optional, example `"e3f4a5b6-c7d8-9012-ef01-345678901234"`): Deal ID - `lead` (object or null, optional): Associated lead data - `lead_id` (string or null, optional, example `"a1b2c3d4-e5f6-7890-abcd-ef1234567890"`): Associated lead ID - `metadata` (object or null, optional): Additional metadata - `notes` (string or null, optional, example `"Initial meeting scheduled for next week"`): Deal notes - `owner_id` (string or null, optional, example `"f4a5b6c7-d8e9-0123-f012-456789012345"`): ID of the deal owner (user) - `probability` (integer or null, optional, example `75`): Win probability percentage (0-100) - `stage_id` (string or null, optional, example `"d2e3f4a5-b6c7-8901-def0-234567890123"`): Current stage ID - `updated_at` (string · date-time or null, optional, example `"2024-01-20T14:45:00+00:00"`): When the deal was last updated - `value` (number or null, optional, example `50000`): Deal value in dollars - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `DEAL_NOT_FOUND` | No deal with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/crm-deals/update-deal # Update a deal `PATCH https://api.versionseven.ai/v1/crm/deals/{deal_id}` Updates the fields you send and leaves the rest unchanged. > **Note:** `custom_fields` sent here is stored in the deal's `metadata`. - Required scope: `crm:write` ## Request **cURL** ```bash curl -X PATCH "https://api.versionseven.ai/v1/crm/deals/a47d043f-fc68-49de-bf05-8861f91f61e8" \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "actual_close_date": "2024-03-15T00:00:00+00:00", "custom_fields": { "priority": "high" }, "expected_close_date": "2024-03-31T00:00:00+00:00", "name": "Acme Corp - Enterprise License", "notes": "Contract under legal review", "owner_id": "f4a5b6c7-d8e9-0123-f012-456789012345", "probability": 75, "stage_id": "d2e3f4a5-b6c7-8901-def0-234567890123", "value": 75000 }' ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/crm/deals/a47d043f-fc68-49de-bf05-8861f91f61e8", { method: "PATCH", headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "actual_close_date": "2024-03-15T00:00:00+00:00", "custom_fields": { "priority": "high" }, "expected_close_date": "2024-03-31T00:00:00+00:00", "name": "Acme Corp - Enterprise License", "notes": "Contract under legal review", "owner_id": "f4a5b6c7-d8e9-0123-f012-456789012345", "probability": 75, "stage_id": "d2e3f4a5-b6c7-8901-def0-234567890123", "value": 75000 }), }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.patch( "https://api.versionseven.ai/v1/crm/deals/a47d043f-fc68-49de-bf05-8861f91f61e8", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, json={ "actual_close_date": "2024-03-15T00:00:00+00:00", "custom_fields": { "priority": "high", }, "expected_close_date": "2024-03-31T00:00:00+00:00", "name": "Acme Corp - Enterprise License", "notes": "Contract under legal review", "owner_id": "f4a5b6c7-d8e9-0123-f012-456789012345", "probability": 75, "stage_id": "d2e3f4a5-b6c7-8901-def0-234567890123", "value": 75000, }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Path parameters - `deal_id` (string · uuid, required): ID of the deal. ## Request body - `actual_close_date` (string · date-time or null, optional, at most 2,000 characters, example `"2024-03-15T00:00:00+00:00"`): Actual close date (set when deal is won/lost) - `custom_fields` (object or null, optional): Custom fields as key-value pairs - `expected_close_date` (string · date-time or null, optional, at most 2,000 characters, example `"2024-03-31T00:00:00+00:00"`): Expected close date - `name` (string or null, optional, at most 2,000 characters, example `"Acme Corp - Enterprise License"`): Deal name/title - `notes` (string or null, optional, at most 2,000 characters, example `"Contract under legal review"`): Deal notes - `owner_id` (string · uuid or null, optional, at most 2,000 characters, example `"f4a5b6c7-d8e9-0123-f012-456789012345"`): ID of the deal owner (user) - `probability` (integer or null, optional, ≥ 0, ≤ 100, example `75`): Win probability percentage (0-100) - `stage_id` (string · uuid or null, optional, at most 2,000 characters, example `"d2e3f4a5-b6c7-8901-def0-234567890123"`): Move deal to a different stage - `value` (number or null, optional, ≥ 0, example `75000`): Deal value in dollars ## Response ### `200` ```json { "message": "string", "deal": { "name": "Acme Corp - Enterprise License", "actual_close_date": "2026-01-14T19:30:00+00:00", "created_at": "2024-01-15T10:30:00+00:00", "custom_fields": {}, "expected_close_date": "2024-03-31T00:00:00+00:00", "id": "e3f4a5b6-c7d8-9012-ef01-345678901234", "lead_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "metadata": {}, "notes": "Initial meeting scheduled for next week", "owner_id": "f4a5b6c7-d8e9-0123-f012-456789012345", "probability": 75, "stage_id": "d2e3f4a5-b6c7-8901-def0-234567890123", "updated_at": "2024-01-20T14:45:00+00:00", "value": 50000 }, "success": true } ``` - `message` (string, required) - `deal` (object, required): The updated deal - `name` (string, required, example `"Acme Corp - Enterprise License"`): Deal name/title - `actual_close_date` (string · date-time or null, optional): Actual close date (set when deal is won/lost) - `created_at` (string · date-time or null, optional, example `"2024-01-15T10:30:00+00:00"`): When the deal was created - `custom_fields` (object or null, optional): Custom fields as key-value pairs - `expected_close_date` (string · date-time or null, optional, example `"2024-03-31T00:00:00+00:00"`): Expected close date - `id` (string or null, optional, example `"e3f4a5b6-c7d8-9012-ef01-345678901234"`): Deal ID - `lead_id` (string or null, optional, example `"a1b2c3d4-e5f6-7890-abcd-ef1234567890"`): Associated lead ID - `metadata` (object or null, optional): Additional metadata - `notes` (string or null, optional, example `"Initial meeting scheduled for next week"`): Deal notes - `owner_id` (string or null, optional, example `"f4a5b6c7-d8e9-0123-f012-456789012345"`): ID of the deal owner (user) - `probability` (integer or null, optional, example `75`): Win probability percentage (0-100) - `stage_id` (string or null, optional, example `"d2e3f4a5-b6c7-8901-def0-234567890123"`): Current stage ID - `updated_at` (string · date-time or null, optional, example `"2024-01-20T14:45:00+00:00"`): When the deal was last updated - `value` (number or null, optional, example `50000`): Deal value in dollars - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. | | 400 | `NO_UPDATES` | The update request contained no fields to change. | | 400 | `INVALID_STAGE` | The `stage_id` in the request body isn't a stage in your organization. This is a `400` rather than a `404` for the same reason as `INVALID_LEAD`. | | 400 | `INVALID_OWNER` | The `owner_id` in the request body isn't a member of your organization. This is a `400` rather than a `404` for the same reason as `INVALID_LEAD`. | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 404 | `DEAL_NOT_FOUND` | No deal with this ID exists in your organization. | | 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. | | 413 | `PAYLOAD_TOO_LARGE` | The request body is larger than 1 MB. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/reference/list-sequence-templates # List sequence templates `GET https://api.versionseven.ai/v1/sequence-templates` Blank `multichannel`, `linkedin_only` and `email_only` sequences. Each passes the structural rules for [Create a campaign](https://docs.versionseven.ai/api-reference/campaigns/create-campaign) and [Replace a campaign's sequence](https://docs.versionseven.ai/api-reference/campaigns/replace-sequence); fill in the empty `message`, `subject` and `content` fields before activating. - Required scope: `campaigns:read` ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/sequence-templates" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/sequence-templates", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/sequence-templates", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Response ### `200` ```json { "templates": {}, "success": true } ``` - `templates` (object, required): channel\_type -> blank sequence skeleton (multichannel, linkedin\_only, email\_only) - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/reference/list-webhook-examples # List webhook examples `GET https://api.versionseven.ai/v1/webhooks/examples` Three example [`prospect_response`](https://docs.versionseven.ai/api-reference/webhooks/prospect-response) deliveries: an email reply, a LinkedIn reply, and a reply on a campaign with both senders connected. Use them to build and test a receiver before pointing a live campaign at it. - Required scope: `campaigns:read` ## Request **cURL** ```bash curl "https://api.versionseven.ai/v1/webhooks/examples" \ -H "Authorization: Bearer $VICTORIA_API_KEY" ``` **Node.js** ```javascript const response = await fetch("https://api.versionseven.ai/v1/webhooks/examples", { headers: { Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`, }, }); const data = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.versionseven.ai/v1/webhooks/examples", headers={ "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}", }, ) data = response.json() ``` ## Headers - `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`. ## Response ### `200` ```json { "examples": [ { "event": "prospect_response", "idempotency_key": "f3a1c9d2b8e7460a91d0a25f3e7b4c1d6f8a92e0b4c1d6f8a92e0b4c1d6f8a92", "sequence_lead_id": "550e8400-e29b-41d4-a716-446655440000", "timestamp": "2025-01-14T19:30:00.123456+00:00", "channel": "email", "sender": { "email": { "account_id": "xyz789-id", "platform_username": "jane@yourcompany.com", "email": "jane@yourcompany.com" }, "linkedin": { "account_id": "abc123-id", "platform_username": "jane-smith-sdr" } }, "lead": { "annual_revenue": 5000000, "company": "Example Corp", "custom_fields": { "product_interest": "Enterprise Plan", "region": "North America" }, "email": "john.doe@example.com", "employee_count": 50, "first_name": "John", "industry": "Technology", "last_name": "Doe", "linkedin_profile": "https://linkedin.com/in/johndoe", "title": "VP of Sales", "website": "https://example.com" }, "ai_response": { "agent_action": "reply", "agent_version": 2, "asset_delivered": true, "asset_url": "https://pages.example.com/p/abc123", "complete": false, "conversation_status": "awaiting_prospect", "escalation_reason": null, "first_message_mode": "asset", "goal": "Schedule a demo", "goal_status": "link_sent", "out_of_office": false, "responder_message": "Thank you for your interest! I'd be happy to schedule a demo for you.", "sdr_brief": "Prospect expressed interest, recommend scheduling demo within 24 hours.", "sentiment": "positive" }, "campaign": "Q1 2024 Outbound Campaign", "conversation_owner": "victoria", "prospect_message": "Hi, I'm interested in learning more about your product.", "variation": "a" } ], "success": true } ``` - `examples` (array of objects, required): Array of example webhook payloads - `event` (string, required, example `"prospect_response"`): Event type - `idempotency_key` (string, required, exactly 64 characters, example `"f3a1c9d2b8e7460a91d0a25f3e7b4c1d6f8a92e0b4c1d6f8a92e0b4c1d6f8a92"`): SHA-256 hex digest (64 characters) of the sequence lead, campaign, channel and prospect message. Identical on every redelivery of the same reply; a different reply from the same prospect gets a new key - `sequence_lead_id` (string · uuid, required, example `"550e8400-e29b-41d4-a716-446655440000"`): ID of the sequence lead this response relates to - `timestamp` (string, required, example `"2025-01-14T19:30:00.123456+00:00"`): When the delivery was built: ISO 8601, UTC, with a `+00:00` offset and microseconds - `channel` (string, required, example `"email"`): Communication channel: 'email' or 'linkedin' - `sender` (object, required): The accounts assigned to send to this lead, keyed by channel. A channel is left out when no account is assigned, and the object is empty when the accounts can't be looked up. - `email` (object or null, optional): Present when the lead has an email account assigned - `account_id` (string, required, example `"xyz789-id"`): ID of the connected email account that sent the outreach. - `platform_username` (string, required, example `"jane@yourcompany.com"`): Account username (typically the address) - `email` (string, required, example `"jane@yourcompany.com"`): Sender email address - `linkedin` (object or null, optional): Present when the lead has a LinkedIn account assigned - `account_id` (string, required, example `"abc123-id"`): ID of the connected LinkedIn account that sent the outreach. - `platform_username` (string, required, example `"jane-smith-sdr"`): LinkedIn username/handle - `lead` (object, required): The lead's details. A field the lead has no value for is `null`. - `annual_revenue` (integer or null, optional, example `5000000`) - `company` (string or null, optional, example `"Example Corp"`) - `custom_fields` (object, optional) - `email` (string or null, optional, example `"john.doe@example.com"`) - `employee_count` (integer or null, optional, example `50`) - `first_name` (string or null, optional, example `"John"`) - `industry` (string or null, optional, example `"Technology"`) - `last_name` (string or null, optional, example `"Doe"`) - `linkedin_profile` (string or null, optional, example `"https://linkedin.com/in/johndoe"`): The lead's LinkedIn profile URL - `title` (string or null, optional, example `"VP of Sales"`) - `website` (string or null, optional, example `"https://example.com"`): The lead's company website - `ai_response` (object, required): The AI Appointment Setter's analysis of the reply. When the reply wasn't analyzed, every field except `goal` is `null`, and `goal` gives the reason. - `agent_action` (string or null, optional, example `"reply"`): What the agent did with the reply: 'reply', 'wait', 'nudge', 'escalate', 'close' or 'skip' - `agent_version` (integer or null, optional, example `2`): Present (as 2) when the Appointment Setter agent handled the reply; absent on legacy payloads. Feature-detect on this field - `asset_delivered` (boolean or null, optional, example `true`): Asset-first campaigns only: whether the page went out - `asset_url` (string or null, optional, example `"https://pages.example.com/p/abc123"`): Asset-first campaigns only: the personalised page that was sent - `complete` (boolean or null, optional, example `false`): `true` when the conversation has ended or should end, whether or not its goal was reached. When `agent_version` is `2`, it's `true` exactly when `conversation_status` is one of the `closed_` values. - `conversation_status` (string or null, optional, example `"awaiting_prospect"`): The conversation's state after this reply. `awaiting_prospect`: waiting for the prospect. `needs_human`: the AI Appointment Setter handed the conversation to your team. `human_owned`: someone on your team has taken it over. `closed_won`, `closed_lost`, `closed_no_response` or `closed_escalated`: the conversation has ended. A lead still on the older follow-up flow can show `awaiting_followup`. Handle values you don't recognize without failing. - `escalation_reason` (string or null, optional): Why the agent handed the conversation to a human, when it did - `first_message_mode` (string or null, optional, example `"asset"`): Asset-first campaigns only: how the first message was sent - `goal` (string or null, optional, example `"Schedule a demo"`): The campaign goal the responder is working toward, or the skip reason - `goal_status` (string or null, optional, example `"link_sent"`): Progress toward the meeting goal: 'not\_sent', 'link\_sent', 'soft\_commit', 'confirmed' or 'declined' - `out_of_office` (boolean or null, optional, example `false`) - `responder_message` (string or null, optional, example `"Thank you for your interest! I'd be happy to schedule a demo for you."`): The reply the responder sent, or null when nothing was sent - `sdr_brief` (string or null, optional, example `"Prospect expressed interest, recommend scheduling demo within 24 hours."`) - `sentiment` (string or null, optional, example `"positive"`): 'positive', 'neutral' or 'negative' - `campaign` (string or null, optional, example `"Q1 2024 Outbound Campaign"`): The campaign's name. The payload doesn't include the campaign's ID. - `conversation_owner` (string or null, optional, example `"victoria"`): Present only when the conversation has an owner: 'victoria' when Victoria's responder is handling replies, 'lp\_responder' when an external responder is. Receivers that reply to prospects themselves should stay out of conversations Victoria owns - `prospect_message` (string or null, optional, example `"Hi, I'm interested in learning more about your product."`): The prospect's reply, as received - `variation` (string or null, optional, example `"a"`): A/B arm the lead is in: 'a', 'b', or null when the lead has none - `success` (boolean, optional, default `true`) ## Errors Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example: ```json { "success": false, "error": "UNAUTHORIZED", "message": "Invalid or inactive API key", "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4" } ``` | Status | Code | Meaning | | --- | --- | --- | | 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. | | 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. | | 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. | | 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. | | 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. | | 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. | | 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. | ## Response headers | Header | Description | | --- | --- | | `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. | | `RateLimit-Limit` | Requests allowed to this endpoint per minute. | | `RateLimit-Remaining` | Requests you can still send to this endpoint right now. | | `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. | | `Retry-After` | On a `429`: seconds to wait before retrying. | --- Source: https://docs.versionseven.ai/api-reference/webhooks/prospect-response # prospect_response webhook event Sent to a campaign's webhooks when a prospect replies. Victoria AI sends this event as an HTTP `POST` with a JSON body when a prospect replies to a campaign. It goes to every enabled webhook on that campaign at the same time. Register one with [Create a webhook](https://docs.versionseven.ai/api-reference/campaigns/create-webhook). ## Delivery - Your endpoint has 75 seconds to respond. An error status or a timeout counts as a failure. - If every webhook on the campaign fails, the delivery is retried every 15 minutes, up to 5 attempts within 48 hours. - A reply is normally delivered once per lead. If a later reply from the same lead is positive after an earlier one wasn't, it's delivered again with a new `idempotency_key`, because the message is part of the key. - Retries of the same delivery carry the same `idempotency_key`. Record the keys you've processed and skip repeats. ## Signatures When the webhook has a signing secret, each delivery carries `X-Signature-256: sha256=`: an HMAC-SHA256 of the raw request body, keyed with the secret. Recompute it over the exact bytes you received, before parsing the JSON. Set or rotate the secret with [Set or rotate a webhook secret](https://docs.versionseven.ai/api-reference/campaigns/rotate-webhook-secret). ## Request headers | Header | Description | | --- | --- | | `Content-Type` | `application/json` | | `X-Signature-256` | `sha256=` followed by the hex HMAC-SHA256 of the raw body, keyed with the webhook's secret. Only sent when the webhook has a secret. | ## Payload - `event` (string, required, example `"prospect_response"`): Event type - `idempotency_key` (string, required, exactly 64 characters, example `"f3a1c9d2b8e7460a91d0a25f3e7b4c1d6f8a92e0b4c1d6f8a92e0b4c1d6f8a92"`): SHA-256 hex digest (64 characters) of the sequence lead, campaign, channel and prospect message. Identical on every redelivery of the same reply; a different reply from the same prospect gets a new key - `sequence_lead_id` (string · uuid, required, example `"550e8400-e29b-41d4-a716-446655440000"`): ID of the sequence lead this response relates to - `timestamp` (string, required, example `"2025-01-14T19:30:00.123456+00:00"`): When the delivery was built: ISO 8601, UTC, with a `+00:00` offset and microseconds - `channel` (string, required, example `"email"`): Communication channel: 'email' or 'linkedin' - `sender` (object, required): The accounts assigned to send to this lead, keyed by channel. A channel is left out when no account is assigned, and the object is empty when the accounts can't be looked up. - `email` (object or null, optional): Present when the lead has an email account assigned - `account_id` (string, required, example `"xyz789-id"`): ID of the connected email account that sent the outreach. - `platform_username` (string, required, example `"jane@yourcompany.com"`): Account username (typically the address) - `email` (string, required, example `"jane@yourcompany.com"`): Sender email address - `linkedin` (object or null, optional): Present when the lead has a LinkedIn account assigned - `account_id` (string, required, example `"abc123-id"`): ID of the connected LinkedIn account that sent the outreach. - `platform_username` (string, required, example `"jane-smith-sdr"`): LinkedIn username/handle - `lead` (object, required): The lead's details. A field the lead has no value for is `null`. - `annual_revenue` (integer or null, optional, example `5000000`) - `company` (string or null, optional, example `"Example Corp"`) - `custom_fields` (object, optional) - `email` (string or null, optional, example `"john.doe@example.com"`) - `employee_count` (integer or null, optional, example `50`) - `first_name` (string or null, optional, example `"John"`) - `industry` (string or null, optional, example `"Technology"`) - `last_name` (string or null, optional, example `"Doe"`) - `linkedin_profile` (string or null, optional, example `"https://linkedin.com/in/johndoe"`): The lead's LinkedIn profile URL - `title` (string or null, optional, example `"VP of Sales"`) - `website` (string or null, optional, example `"https://example.com"`): The lead's company website - `ai_response` (object, required): The AI Appointment Setter's analysis of the reply. When the reply wasn't analyzed, every field except `goal` is `null`, and `goal` gives the reason. - `agent_action` (string or null, optional, example `"reply"`): What the agent did with the reply: 'reply', 'wait', 'nudge', 'escalate', 'close' or 'skip' - `agent_version` (integer or null, optional, example `2`): Present (as 2) when the Appointment Setter agent handled the reply; absent on legacy payloads. Feature-detect on this field - `asset_delivered` (boolean or null, optional, example `true`): Asset-first campaigns only: whether the page went out - `asset_url` (string or null, optional, example `"https://pages.example.com/p/abc123"`): Asset-first campaigns only: the personalised page that was sent - `complete` (boolean or null, optional, example `false`): `true` when the conversation has ended or should end, whether or not its goal was reached. When `agent_version` is `2`, it's `true` exactly when `conversation_status` is one of the `closed_` values. - `conversation_status` (string or null, optional, example `"awaiting_prospect"`): The conversation's state after this reply. `awaiting_prospect`: waiting for the prospect. `needs_human`: the AI Appointment Setter handed the conversation to your team. `human_owned`: someone on your team has taken it over. `closed_won`, `closed_lost`, `closed_no_response` or `closed_escalated`: the conversation has ended. A lead still on the older follow-up flow can show `awaiting_followup`. Handle values you don't recognize without failing. - `escalation_reason` (string or null, optional): Why the agent handed the conversation to a human, when it did - `first_message_mode` (string or null, optional, example `"asset"`): Asset-first campaigns only: how the first message was sent - `goal` (string or null, optional, example `"Schedule a demo"`): The campaign goal the responder is working toward, or the skip reason - `goal_status` (string or null, optional, example `"link_sent"`): Progress toward the meeting goal: 'not\_sent', 'link\_sent', 'soft\_commit', 'confirmed' or 'declined' - `out_of_office` (boolean or null, optional, example `false`) - `responder_message` (string or null, optional, example `"Thank you for your interest! I'd be happy to schedule a demo for you."`): The reply the responder sent, or null when nothing was sent - `sdr_brief` (string or null, optional, example `"Prospect expressed interest, recommend scheduling demo within 24 hours."`) - `sentiment` (string or null, optional, example `"positive"`): 'positive', 'neutral' or 'negative' - `campaign` (string or null, optional, example `"Q1 2024 Outbound Campaign"`): The campaign's name. The payload doesn't include the campaign's ID. - `conversation_owner` (string or null, optional, example `"victoria"`): Present only when the conversation has an owner: 'victoria' when Victoria's responder is handling replies, 'lp\_responder' when an external responder is. Receivers that reply to prospects themselves should stay out of conversations Victoria owns - `prospect_message` (string or null, optional, example `"Hi, I'm interested in learning more about your product."`): The prospect's reply, as received - `variation` (string or null, optional, example `"a"`): A/B arm the lead is in: 'a', 'b', or null when the lead has none ## Examples ### Email reply ```json { "event": "prospect_response", "idempotency_key": "f3a1c9d2b8e7460a91d0a25f3e7b4c1d6f8a92e0b4c1d6f8a92e0b4c1d6f8a92", "sequence_lead_id": "550e8400-e29b-41d4-a716-446655440000", "timestamp": "2025-01-14T19:30:00.482913+00:00", "campaign": "Q1 2024 Outbound Campaign", "variation": "a", "channel": "email", "sender": { "email": { "account_id": "email-abc123", "platform_username": "jane@yourcompany.com", "email": "jane@yourcompany.com" } }, "lead": { "first_name": "John", "last_name": "Doe", "email": "john.doe@example.com", "company": "Example Corp", "title": "VP of Sales", "linkedin_profile": "https://linkedin.com/in/johndoe", "annual_revenue": 5000000, "employee_count": 50, "industry": "Technology", "website": "https://example.com", "custom_fields": { "region": "North America", "product_interest": "Enterprise Plan" } }, "prospect_message": "Hi, I'm interested in learning more about your product.", "ai_response": { "sentiment": "positive", "out_of_office": false, "complete": false, "goal": "Schedule a demo", "responder_message": "Thank you for your interest! I'd be happy to schedule a demo for you.", "sdr_brief": "Prospect expressed interest, recommend scheduling demo within 24 hours." } } ``` ### LinkedIn reply ```json { "event": "prospect_response", "idempotency_key": "a7b2e5f9c1d4068b3e7f2a5c8d1e4b7a0c3f6e9b2d5a8c1f4e7b0d3a6c9f2e5b", "sequence_lead_id": "660f9511-f3a0-42e5-b827-557766551111", "timestamp": "2025-01-15T10:15:00.117204+00:00", "campaign": "Enterprise Tech Outreach", "variation": "b", "channel": "linkedin", "sender": { "linkedin": { "account_id": "li-def456", "platform_username": "alex-sdr-yourcompany" } }, "lead": { "first_name": "Sarah", "last_name": "Chen", "email": "sarah.chen@techstartup.io", "company": "TechStartup Inc", "title": "CTO", "linkedin_profile": "https://linkedin.com/in/sarahchen", "annual_revenue": 12000000, "employee_count": 85, "industry": "Software", "website": "https://techstartup.io", "custom_fields": { "region": "West Coast", "product_interest": "API Integration" } }, "prospect_message": "This looks interesting. Can you send me some case studies?", "ai_response": { "sentiment": "positive", "out_of_office": false, "complete": false, "goal": "Share case studies", "responder_message": "Absolutely! I'll send over a few case studies from similar companies in your industry.", "sdr_brief": "Prospect requesting more information, warm lead showing buying signals." }, "conversation_owner": "victoria" } ``` ### Out of office ```json { "event": "prospect_response", "idempotency_key": "b8c3f6e0d2e5179c4f8a3b6d9e2f5a8c1b4d7e0a3c6f9b2e5d8a1c4f7b0d3e6a", "sequence_lead_id": "770a0622-04b1-43f6-8938-668877662222", "timestamp": "2025-01-16T14:45:00.903316+00:00", "campaign": "SMB Growth Initiative", "variation": "a", "channel": "email", "sender": { "linkedin": { "account_id": "li-ghi789", "platform_username": "morgan-sdr-yourcompany" }, "email": { "account_id": "email-jkl012", "platform_username": "morgan@yourcompany.com", "email": "morgan@yourcompany.com" } }, "lead": { "first_name": "Michael", "last_name": "Roberts", "email": "m.roberts@retailco.com", "company": "RetailCo", "title": "Director of Operations", "linkedin_profile": "https://linkedin.com/in/michaelroberts", "annual_revenue": 25000000, "employee_count": 200, "industry": "Retail", "website": "https://retailco.com", "custom_fields": { "region": "Midwest", "product_interest": "Automation Tools" } }, "prospect_message": "I'll be out of office until January 20th. Please reach out then.", "ai_response": { "sentiment": "neutral", "out_of_office": true, "complete": false, "goal": "Follow up after OOO", "responder_message": null, "sdr_brief": "Prospect is OOO until Jan 20. Schedule follow-up for that date.", "agent_version": 2, "agent_action": "wait", "conversation_status": "awaiting_followup", "goal_status": "not_sent", "escalation_reason": null } } ``` --- Source: https://docs.versionseven.ai/cookbook # Cookbook End-to-end recipes that combine several endpoints into a working integration. - [Add CRM contacts to a campaign](https://docs.versionseven.ai/cookbook/add-crm-contacts): A script that adds contacts exported from your CRM to a Victoria AI campaign, safe to re-run, handling existing leads, invalid rows and rate limits. - [Post prospect replies to Slack](https://docs.versionseven.ai/cookbook/post-replies-to-slack): A small server that receives the prospect_response webhook, verifies its signature, skips duplicates, and posts the reply to a Slack channel. --- Source: https://docs.versionseven.ai/cookbook/add-crm-contacts # Add CRM contacts to a campaign A script that adds contacts exported from your CRM to a Victoria AI campaign, safe to re-run, handling existing leads, invalid rows and rate limits. Endpoints used: - [`GET /v1/campaigns`](https://docs.versionseven.ai/api-reference/campaigns/list-campaigns) List campaigns - [`POST /v1/leads`](https://docs.versionseven.ai/api-reference/leads/create-lead) Create a lead This recipe takes a list of contacts from your CRM, such as an export or the result of your CRM's own API, and adds each one to a Victoria AI campaign with [`POST /v1/leads`](https://docs.versionseven.ai/api-reference/leads/create-lead). Run it as often as you like: a contact that's already a lead is enrolled rather than duplicated, and one that's already in the campaign is skipped. ## Before you start - An API key in the `VICTORIA_API_KEY` environment variable. See [Authentication](https://docs.versionseven.ai/guides/authentication). - The name of the campaign to add leads to. It can be a draft, paused or active campaign. Each lead starts in the campaign's backlog. - Node.js 18 or later, or Python 3.10 or later with `requests`. ## How it works 1. Find the campaign by name with [`GET /v1/campaigns`](https://docs.versionseven.ai/api-reference/campaigns/list-campaigns). 2. Turn each contact into a lead. A lead needs `first_name`, `last_name`, and an `email` or a `linkedin_url`. 3. Send it with an `Idempotency-Key` made from the campaign and the lead, so a retried request can't enrol the lead twice. 4. Handle the response, pausing between requests to stay under the rate limit of 100 requests a minute to one endpoint. ## Contacts The script reads a JSON file of contacts. Map your CRM's field names to these: ```json [ { "first_name": "Sarah", "last_name": "Johnson", "email": "sarah.johnson@acmecorp.com", "company": "Acme Corp", "title": "VP of Sales" }, { "first_name": "Marcus", "last_name": "Lee", "linkedin_url": "https://www.linkedin.com/in/marcuslee/", "company": "Northwind" } ] ``` A lead can also carry `company_website`, `industry`, `annual_revenue`, `employees` and `custom_fields`. See [`POST /v1/leads`](https://docs.versionseven.ai/api-reference/leads/create-lead) for every field. ## The script **Node.js** ```javascript // add-contacts.mjs import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; const API_URL = "https://api.versionseven.ai/v1"; const API_KEY = process.env.VICTORIA_API_KEY; const PAUSE_MS = 650; // about 90 requests a minute const MAX_ATTEMPTS = 5; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // Sends a request, waiting and retrying on 429, 503 and IDEMPOTENCY_IN_PROGRESS. async function call(method, path, body, headers = {}) { for (let attempt = 1; ; attempt++) { const response = await fetch(`${API_URL}${path}`, { method, headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json", ...headers, }, body: body === undefined ? undefined : JSON.stringify(body), }); const data = await response.json().catch(() => ({})); const retryable = response.status === 429 || response.status === 503 || data.error === "IDEMPOTENCY_IN_PROGRESS"; if (!retryable || attempt === MAX_ATTEMPTS) return { status: response.status, data }; const waitSeconds = Number(response.headers.get("Retry-After")) || 2 ** attempt; await sleep(waitSeconds * 1000); } } async function findCampaign(name) { for (let offset = 0; ; ) { const { status, data } = await call("GET", `/campaigns?limit=500&offset=${offset}`); if (status !== 200) throw new Error(`Listing campaigns failed: ${status} ${data.error}`); const campaign = data.campaigns.find((candidate) => candidate.name === name); if (campaign) return campaign; if (!data.has_more) throw new Error(`No campaign named "${name}"`); offset += data.count; } } function toLead(contact) { const lead = {}; for (const field of ["first_name", "last_name", "email", "company", "title"]) { if (contact[field]) lead[field] = contact[field].trim(); } // LinkedIn URLs are matched exactly, so send them in one consistent form. if (contact.linkedin_url) lead.linkedin_url = contact.linkedin_url.trim().replace(/\/+$/, ""); return lead; } const [campaignName, contactsFile] = process.argv.slice(2); const campaign = await findCampaign(campaignName); const contacts = JSON.parse(await readFile(contactsFile, "utf8")); const counts = { created: 0, enrolled: 0, skipped: 0, invalid: 0, failed: 0 }; for (const contact of contacts) { const body = { campaign_id: campaign.id, lead: toLead(contact) }; // The same campaign and lead always make the same key. const idempotencyKey = createHash("sha256").update(JSON.stringify(body)).digest("hex"); const { status, data } = await call("POST", "/leads", body, { "Idempotency-Key": idempotencyKey, }); const who = body.lead.email ?? body.lead.linkedin_url ?? "(no email or LinkedIn URL)"; if (status === 201) { counts.created++; console.log(`created ${who} → lead ${data.lead_id}`); } else if (status === 200) { counts.enrolled++; console.log(`enrolled ${who} → existing lead ${data.lead_id}`); } else if (data.error === "LEAD_ALREADY_IN_CAMPAIGN" || data.error === "LEAD_ALREADY_EXISTS") { counts.skipped++; console.log(`skipped ${who}: ${data.error}`); } else if (data.error === "VALIDATION_ERROR") { counts.invalid++; const errors = data.details?.errors ?? []; const problems = errors.map((error) => `${error.field} ${error.message}`); console.log(`invalid ${who}: ${problems.join("; ")}`); } else if (data.error === "CAMPAIGN_NOT_FOUND") { throw new Error(`Campaign ${campaign.id} no longer exists`); } else { counts.failed++; console.log(`failed ${who}: ${status} ${data.error} (request_id ${data.request_id})`); } await sleep(PAUSE_MS); } console.log(counts); ``` **Python** ```python # add_contacts.py import hashlib import json import os import sys import time import requests API_URL = "https://api.versionseven.ai/v1" HEADERS = {"Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}"} PAUSE_SECONDS = 0.65 # about 90 requests a minute MAX_ATTEMPTS = 5 def call(method, path, body=None, headers=None): """Sends a request, waiting and retrying on 429, 503 and IDEMPOTENCY_IN_PROGRESS.""" for attempt in range(1, MAX_ATTEMPTS + 1): response = requests.request( method, f"{API_URL}{path}", headers={**HEADERS, **(headers or {})}, json=body, timeout=30, ) try: data = response.json() except ValueError: data = {} retryable = ( response.status_code in (429, 503) or data.get("error") == "IDEMPOTENCY_IN_PROGRESS" ) if not retryable or attempt == MAX_ATTEMPTS: return response.status_code, data time.sleep(float(response.headers.get("Retry-After") or 2**attempt)) def find_campaign(name): offset = 0 while True: status, data = call("GET", f"/campaigns?limit=500&offset={offset}") if status != 200: sys.exit(f"Listing campaigns failed: {status} {data.get('error')}") for campaign in data["campaigns"]: if campaign["name"] == name: return campaign if not data["has_more"]: sys.exit(f'No campaign named "{name}"') offset += data["count"] def to_lead(contact): lead = {} for field in ("first_name", "last_name", "email", "company", "title"): if contact.get(field): lead[field] = contact[field].strip() # LinkedIn URLs are matched exactly, so send them in one consistent form. if contact.get("linkedin_url"): lead["linkedin_url"] = contact["linkedin_url"].strip().rstrip("/") return lead def main(): campaign_name, contacts_file = sys.argv[1], sys.argv[2] campaign = find_campaign(campaign_name) with open(contacts_file, encoding="utf-8") as file: contacts = json.load(file) counts = {"created": 0, "enrolled": 0, "skipped": 0, "invalid": 0, "failed": 0} for contact in contacts: body = {"campaign_id": campaign["id"], "lead": to_lead(contact)} # The same campaign and lead always make the same key. serialized = json.dumps(body, sort_keys=True).encode() idempotency_key = hashlib.sha256(serialized).hexdigest() status, data = call("POST", "/leads", body, {"Idempotency-Key": idempotency_key}) lead = body["lead"] who = lead.get("email") or lead.get("linkedin_url") or "(no email or LinkedIn URL)" error = data.get("error") if status == 201: counts["created"] += 1 print(f"created {who} → lead {data['lead_id']}") elif status == 200: counts["enrolled"] += 1 print(f"enrolled {who} → existing lead {data['lead_id']}") elif error in ("LEAD_ALREADY_IN_CAMPAIGN", "LEAD_ALREADY_EXISTS"): counts["skipped"] += 1 print(f"skipped {who}: {error}") elif error == "VALIDATION_ERROR": counts["invalid"] += 1 errors = (data.get("details") or {}).get("errors", []) problems = "; ".join(f"{e['field']} {e['message']}" for e in errors) print(f"invalid {who}: {problems}") elif error == "CAMPAIGN_NOT_FOUND": sys.exit(f"Campaign {campaign['id']} no longer exists") else: counts["failed"] += 1 print(f"failed {who}: {status} {error} (request_id {data.get('request_id')})") time.sleep(PAUSE_SECONDS) print(counts) if __name__ == "__main__": main() ``` Run it with the campaign's name and the contacts file: ```bash node add-contacts.mjs "Q3 outbound" contacts.json python add_contacts.py "Q3 outbound" contacts.json ``` ## What each response means | Response | What the script does | | - | - | | `201` | A new lead was created and enrolled. The script prints its `lead_id`. | | `200` | The contact was already a lead in your organization, and that lead was enrolled. `lead_created` is `false`. | | `409 LEAD_ALREADY_IN_CAMPAIGN` | Skips the contact: its lead is already in this campaign. | | `409 LEAD_ALREADY_EXISTS` | Skips the contact. With a `campaign_id` in the body, this only happens when an identical request created the lead at the same moment. | | `400 VALIDATION_ERROR` | Skips the contact and prints each problem from `details.errors`, such as a missing `last_name` or a malformed `email`. | | `404 CAMPAIGN_NOT_FOUND` | Stops, because the campaign was deleted while the script ran. | | `429 RATE_LIMITED` or `503` | Waits for `Retry-After` seconds when the response has one, and retries up to 5 attempts. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). | | `409 IDEMPOTENCY_IN_PROGRESS` | Waits and retries: an earlier attempt for the same lead is still running. | ## How existing leads are matched A contact matches an existing lead when its `email` or its `linkedin_url` is already on a lead in your organization: - Emails are compared without regard to case or surrounding spaces, so `Sarah.Johnson@acmecorp.com` finds `sarah.johnson@acmecorp.com`. - LinkedIn URLs are compared exactly, so a URL with a trailing slash is a different URL. The script removes trailing slashes; whatever form you choose, use it every time. A matched lead is enrolled as it's stored. The name, company and other fields in the request don't update it; to change them, use [`PATCH /v1/leads/{lead_id}`](https://docs.versionseven.ai/api-reference/leads/update-lead). ## Re-running the script The `Idempotency-Key` is a SHA-256 hash of the request body: the campaign ID and the lead's fields. Running the script again with the same contacts sends the same keys, and nothing is enrolled twice: - While the API still remembers a key, for at least 24 hours, the request returns the first response again, with the header `Idempotent-Replay: true`. - After that, the lead is already in the campaign, so the request answers `409 LEAD_ALREADY_IN_CAMPAIGN`. When a contact's details change in your CRM, the body and so the key change too. The request then goes through as new and is matched to the existing lead by email or LinkedIn URL, instead of failing with `409 IDEMPOTENCY_KEY_REUSED`. See [Idempotency](https://docs.versionseven.ai/guides/idempotency). ## Next steps - When the campaign's sequence is ready, activate it with [`PATCH /v1/campaigns/{campaign_id}`](https://docs.versionseven.ai/api-reference/campaigns/update-campaign) and `{"is_active": true}`. A sequence with blank steps answers `409 CAMPAIGN_NOT_READY`, with each problem in `details.errors`. - [Post prospect replies to Slack](https://docs.versionseven.ai/cookbook/post-replies-to-slack), to hear when the new leads reply. --- Source: https://docs.versionseven.ai/cookbook/post-replies-to-slack # Post prospect replies to Slack A small server that receives the prospect_response webhook, verifies its signature, skips duplicates, and posts the reply to a Slack channel. Endpoints used: - [`POST /v1/campaigns/{campaign_id}/webhooks`](https://docs.versionseven.ai/api-reference/campaigns/create-webhook) Create a webhook - [`GET /v1/webhooks/examples`](https://docs.versionseven.ai/api-reference/reference/list-webhook-examples) List webhook examples When a prospect replies to a campaign, Victoria AI sends a [`prospect_response`](https://docs.versionseven.ai/api-reference/webhooks/prospect-response) event to the campaign's webhooks. This recipe is a small server that receives the event and posts it to a Slack channel, so your team sees replies without opening Victoria AI. ## Before you start - A Slack [incoming webhook](https://api.slack.com/messaging/webhooks) URL for the channel, in the `SLACK_WEBHOOK_URL` environment variable. - A signing secret of your own, at least 16 characters, in `VICTORIA_WEBHOOK_SECRET`. One way to make one is `openssl rand -hex 32`. - A public HTTPS address for the server. Victoria AI doesn't deliver to `localhost` or private addresses. - Node.js 18 or later with `express`, or Python 3.10 or later with `flask` and `requests`. ## 1. Run the receiver **Node.js (Express)** ```javascript // server.mjs import crypto from "node:crypto"; import express from "express"; const secret = process.env.VICTORIA_WEBHOOK_SECRET; const slackWebhookUrl = process.env.SLACK_WEBHOOK_URL; // Keys already posted. In production, store them in a database or Redis. const processed = new Set(); function isValidSignature(rawBody, header) { if (typeof header !== "string") return false; const digest = crypto.createHmac("sha256", secret).update(rawBody).digest("hex"); const expected = Buffer.from(`sha256=${digest}`); const received = Buffer.from(header); return expected.length === received.length && crypto.timingSafeEqual(expected, received); } // Slack treats &, < and > as control characters in message text. function escapeForSlack(text) { return String(text).replace(/&/g, "&").replace(//g, ">"); } function slackMessage(event) { const lead = event.lead ?? {}; const name = [lead.first_name, lead.last_name].filter(Boolean).join(" ") || "A prospect"; const who = lead.company ? `${name} (${lead.company})` : name; const channel = event.channel === "linkedin" ? "on LinkedIn" : "by email"; const reply = event.prospect_message ?? ""; const excerpt = reply.length > 1000 ? `${reply.slice(0, 1000)}…` : reply; const sentiment = event.ai_response?.sentiment; const campaign = escapeForSlack(event.campaign ?? "a campaign"); const lines = [`*${escapeForSlack(who)}* replied ${channel} to *${campaign}*`]; if (sentiment) lines.push(`Sentiment: ${escapeForSlack(sentiment)}`); lines.push(...escapeForSlack(excerpt).split("\n").map((line) => `> ${line}`)); return { text: lines.join("\n") }; } const app = express(); // express.raw keeps the body as the exact bytes that were signed. const rawJson = express.raw({ type: "application/json" }); app.post("/victoria/webhooks", rawJson, async (req, res) => { if (!isValidSignature(req.body, req.get("X-Signature-256"))) return res.sendStatus(401); const event = JSON.parse(req.body.toString("utf8")); if (event.event !== "prospect_response" || processed.has(event.idempotency_key)) { return res.sendStatus(200); } try { const slack = await fetch(slackWebhookUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(slackMessage(event)), signal: AbortSignal.timeout(10_000), }); if (!slack.ok) throw new Error(`Slack answered ${slack.status}`); } catch (error) { console.error(`Couldn't post to Slack: ${error.message}`); // An error status marks the delivery as failed, so Victoria AI can retry it. return res.sendStatus(502); } processed.add(event.idempotency_key); res.sendStatus(200); }); app.listen(3000, () => console.log("Listening on port 3000")); ``` **Python (Flask)** ```python # server.py import hashlib import hmac import os import requests from flask import Flask, abort, request app = Flask(__name__) SECRET = os.environ["VICTORIA_WEBHOOK_SECRET"].encode() SLACK_WEBHOOK_URL = os.environ["SLACK_WEBHOOK_URL"] # Keys already posted. In production, store them in a database or Redis. processed = set() def is_valid_signature(raw_body: bytes, header: str | None) -> bool: if not header: return False expected = "sha256=" + hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, header) def escape_for_slack(text) -> str: # Slack treats &, < and > as control characters in message text. return str(text).replace("&", "&").replace("<", "<").replace(">", ">") def slack_message(event: dict) -> dict: lead = event.get("lead") or {} parts = (lead.get("first_name"), lead.get("last_name")) name = " ".join(part for part in parts if part) or "A prospect" who = f"{name} ({lead['company']})" if lead.get("company") else name channel = "on LinkedIn" if event.get("channel") == "linkedin" else "by email" reply = event.get("prospect_message") or "" excerpt = reply[:1000] + "…" if len(reply) > 1000 else reply sentiment = (event.get("ai_response") or {}).get("sentiment") campaign = event.get("campaign") or "a campaign" lines = [f"*{escape_for_slack(who)}* replied {channel} to *{escape_for_slack(campaign)}*"] if sentiment: lines.append(f"Sentiment: {escape_for_slack(sentiment)}") lines.extend(f"> {line}" for line in escape_for_slack(excerpt).split("\n")) return {"text": "\n".join(lines)} @app.post("/victoria/webhooks") def victoria_webhook(): raw_body = request.get_data() # the exact bytes that were signed if not is_valid_signature(raw_body, request.headers.get("X-Signature-256")): abort(401) event = request.get_json() if event.get("event") != "prospect_response" or event.get("idempotency_key") in processed: return "", 200 try: slack = requests.post(SLACK_WEBHOOK_URL, json=slack_message(event), timeout=10) slack.raise_for_status() except requests.RequestException as error: app.logger.error("Couldn't post to Slack: %s", error) # An error status marks the delivery as failed, so Victoria AI can retry it. abort(502) processed.add(event["idempotency_key"]) return "", 200 ``` Start it on port 3000 with `node server.mjs`, or `flask --app server run --port 3000`. In production, run the Flask app under a WSGI server such as Gunicorn. The receiver: - Rejects a request whose `X-Signature-256` doesn't match the raw body with `401`. See [Verifying signatures](https://docs.versionseven.ai/guides/verifying-webhooks). - Skips an event it has already posted. Retries of a delivery carry the same `idempotency_key`. - Posts to Slack before it answers. Victoria AI waits up to 75 seconds for a response, and the Slack request gives up after 10. - Answers `502` when Slack fails, so the delivery counts as failed and can be retried. ## 2. Register the webhook Register the receiver's URL on the campaign with [`POST /v1/campaigns/{campaign_id}/webhooks`](https://docs.versionseven.ai/api-reference/campaigns/create-webhook), sending your secret: ```bash curl -X POST https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/webhooks \ -H "Authorization: Bearer $VICTORIA_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"webhook_url\": \"https://replies.example.com/victoria/webhooks\", \"secret\": \"$VICTORIA_WEBHOOK_SECRET\"}" ``` A `201` means the webhook is created and already active. Because you sent the secret, the response doesn't include one. Webhooks belong to one campaign, so register the URL on each campaign whose replies you want in Slack. ## 3. Send a test delivery Before a real reply arrives, sign a payload yourself and send it to the receiver. This one is shortened; [`GET /v1/webhooks/examples`](https://docs.versionseven.ai/api-reference/reference/list-webhook-examples) returns complete examples. ```bash BODY='{"event":"prospect_response","idempotency_key":"f215faf9d88b7f0a881632ee22459ee452a296c808d261b6cc993d3a1fd0600e","campaign":"Q3 outbound","channel":"email","lead":{"first_name":"Sarah","last_name":"Johnson","company":"Acme Corp"},"prospect_message":"Sounds interesting. Could you send over some times next week?","ai_response":{"sentiment":"positive"}}' SIGNATURE=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$VICTORIA_WEBHOOK_SECRET" | sed 's/^.* //') curl -X POST http://localhost:3000/victoria/webhooks \ -H "Content-Type: application/json" \ -H "X-Signature-256: sha256=$SIGNATURE" \ --data "$BODY" ``` The message appears in Slack. Send the same request again and nothing new is posted, because the key was already processed. Change a character in `BODY` without signing it again, and the receiver answers `401`. ## What to expect from deliveries - A lead's reply is normally delivered once per campaign, not for every message in the conversation. If a later reply is positive after an earlier one wasn't, it's delivered again with a new `idempotency_key`. - `campaign` is the campaign's name; the payload doesn't include its ID. - `ai_response.sentiment` is `null` when the reply wasn't analyzed, so the message leaves the sentiment line out. Lead fields without a value are `null` too. - A failed delivery is retried every 15 minutes, up to 5 attempts within 48 hours. > **Note:** Retries only happen when every webhook on the campaign failed. If another webhook on the same campaign accepted the delivery, a failed Slack post isn't retried. The [`prospect_response` reference](https://docs.versionseven.ai/api-reference/webhooks/prospect-response) documents every field.