API reference
Early access. The VeraGen API is not open yet: this reference shows what it will do, and keys cannot be created until it opens. Request early access.

VeraGen API

Automate VeraGen from your own code.

Introduction

The VeraGen API lets your own code act on your VeraGen account: list your workflows, start runs and follow them, read your projects and Data Management, create pages, keep your Thrive contacts in step with your other systems, and read what the account has been billed.

It is a JSON API over HTTPS. Every request is made with an API key that an owner or admin of the account creates in the hub. A key acts for exactly one account, the one it was created in.

The base URL for every endpoint is:

https://api.veragen.ai/v1

cURL
curl "https://api.veragen.ai/v1/workflows?limit=25" \
  -H "Authorization: Bearer $VERAGEN_API_KEY"
JavaScript
const res = await fetch("https://api.veragen.ai/v1/workflows?limit=25", {
  headers: { Authorization: `Bearer ${process.env.VERAGEN_API_KEY}` },
});
const data = await res.json();
Python
import os, requests

res = requests.get(
    "https://api.veragen.ai/v1/workflows",
    params={"limit": 25},
    headers={"Authorization": f"Bearer {os.environ['VERAGEN_API_KEY']}"},
)
data = res.json()
Response 200
{
  "data": [
    {
      "id": "3f6c1d2e-8a4b-4f7e-9c21-5d0e7a1b2c3d",
      "name": "Weekly competitor digest",
      "description": "Reads five competitor blogs and writes a summary page.",
      "status": "active",
      "project_id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
      "updated_at": "2026-10-02T15:04:05.123Z",
      "last_run": {
        "id": "c0ffee00-1234-4abc-9def-0123456789ab",
        "status": "succeeded",
        "at": "2026-10-03T06:00:00.000Z"
      }
    },
    {
      "id": "7b2e9f40-6c1d-4e8a-b3f5-2a9d0c7e1f64",
      "name": "Lead enrichment",
      "description": "",
      "status": "draft",
      "project_id": null,
      "updated_at": "2026-09-30T09:12:44.000Z",
      "last_run": null
    }
  ],
  "has_more": true,
  "next_cursor": "WyIyMDI2LTA5LTMwVDA5OjEyOjQ0LjAwMDAwMFoiLCI3YjJlOWY0MC02YzFkLTRlOGEtYjNmNS0yYTlkMGM3ZTFmNjQiXQ"
}

Quickstart

  1. In the hub, go to Account → API keys and click Create key. Choose Read only for now. Copy the key: it is shown once.
  2. Keep it out of your code. Put it in an environment variable: export VERAGEN_API_KEY="vg_live_…"
  3. Make your first request. The examples list the workflows in your account; choose your language at the top.

A 200 with a data list means you are connected. A 401 means the key was not sent, was mistyped, or has been revoked.

cURL
curl "https://api.veragen.ai/v1/workflows?limit=25" \
  -H "Authorization: Bearer $VERAGEN_API_KEY"
JavaScript
const res = await fetch("https://api.veragen.ai/v1/workflows?limit=25", {
  headers: { Authorization: `Bearer ${process.env.VERAGEN_API_KEY}` },
});
const data = await res.json();
Python
import os, requests

res = requests.get(
    "https://api.veragen.ai/v1/workflows",
    params={"limit": 25},
    headers={"Authorization": f"Bearer {os.environ['VERAGEN_API_KEY']}"},
)
data = res.json()
Response 200
{
  "data": [
    {
      "id": "3f6c1d2e-8a4b-4f7e-9c21-5d0e7a1b2c3d",
      "name": "Weekly competitor digest",
      "description": "Reads five competitor blogs and writes a summary page.",
      "status": "active",
      "project_id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
      "updated_at": "2026-10-02T15:04:05.123Z",
      "last_run": {
        "id": "c0ffee00-1234-4abc-9def-0123456789ab",
        "status": "succeeded",
        "at": "2026-10-03T06:00:00.000Z"
      }
    },
    {
      "id": "7b2e9f40-6c1d-4e8a-b3f5-2a9d0c7e1f64",
      "name": "Lead enrichment",
      "description": "",
      "status": "draft",
      "project_id": null,
      "updated_at": "2026-09-30T09:12:44.000Z",
      "last_run": null
    }
  ],
  "has_more": true,
  "next_cursor": "WyIyMDI2LTA5LTMwVDA5OjEyOjQ0LjAwMDAwMFoiLCI3YjJlOWY0MC02YzFkLTRlOGEtYjNmNS0yYTlkMGM3ZTFmNjQiXQ"
}

Authentication

Send your key in the Authorization header of every request:

Authorization: Bearer vg_live_…

Keys start with vg_live_. An account can have up to 10 active keys; make one per system that calls the API, so you can revoke one without stopping the others.

Scopes

A key can only call endpoints its scopes allow. Asking for something outside them is a 403 with the code insufficient_scope, and the message names the scope that is missing. A key can reach everything in the account that its scopes allow, in every project.

ScopeLets the key
workflows:readList and read workflows and their runs.
workflows:runStart workflow runs. Runs spend the account's AI credit.
content:readRead projects and Data Management items.
content:writeCreate pages in Data Management.
contacts:readRead Thrive contacts.
contacts:writeCreate and update Thrive contacts.
usage:readRead the account's billed usage.

Keep keys secret

  • Call the API only from a server, a script or an automation tool you control. The API does not answer browsers (it sends no CORS headers), so a key placed in a web page or a mobile app will not work, and anyone can read it.
  • Store keys in a password manager or your platform's secrets, never in source code.
  • If a key leaks, revoke it in Account → API keys. It stops working on its next request. Then create a new one.
cURL
curl "https://api.veragen.ai/v1/workflows" \
  -H "Authorization: Bearer $VERAGEN_API_KEY"
JavaScript
const res = await fetch("https://api.veragen.ai/v1/workflows", {
  headers: { Authorization: `Bearer ${process.env.VERAGEN_API_KEY}` },
});
if (res.status === 401) throw new Error("Check the API key");
Python
import os, requests

res = requests.get(
    "https://api.veragen.ai/v1/workflows",
    headers={"Authorization": f"Bearer {os.environ['VERAGEN_API_KEY']}"},
)
if res.status_code == 401:
    raise SystemExit("Check the API key")
Response 401
{
  "error": {
    "type": "authentication",
    "code": "invalid_api_key",
    "message": "The API key is missing or not valid.",
    "request_id": "req_5f0c2a9e1b7d4c3a8e6f1d20"
  }
}

Errors

Every error has the same shape, with an HTTP status that says what kind of problem it is:

  • type: the kind of error, which never changes for a given code;
  • code: what went wrong, for your code to act on;
  • message: a sentence for a person;
  • param: the parameter at fault, when there is one;
  • resource_id: on 409 contact_exists, the id of the contact that already has the email address;
  • request_id: also sent as the X-Request-Id header on every response. Quote it to VeraGen support.

An id that belongs to another account gets exactly the answer an id that does not exist gets: 404 resource_not_found. Unknown query parameters are refused with 400 unknown_parameter, and unknown body fields with 422 unknown_field, never silently ignored, so a typo cannot quietly change what you asked for. An update (PATCH) changes only the fields you send; send null to clear one.

StatusCodeMeaning
400 invalid_requestinvalid_parameterA query parameter has a value it cannot take. param names it.
400 invalid_requestunknown_parameterA query parameter this endpoint does not accept. Unknown parameters are refused, never ignored.
400 invalid_requestinvalid_idAn id that is not a UUID.
400 invalid_requestinvalid_cursorThe cursor is not one this endpoint gave out.
400 invalid_requestinvalid_jsonThe body is not valid JSON.
400 invalid_requestmissing_idempotency_keyStarting a run needs an Idempotency-Key header.
400 invalid_requestmissing_fieldA required body field is missing. param names it.
400 invalid_requestinvalid_idempotency_keyThe Idempotency-Key is not 8 to 64 printable characters without spaces.
401 authenticationinvalid_api_keyNo API key, a malformed one, or one that does not exist.
401 authenticationapi_key_revokedThe key was revoked in the hub.
401 authenticationapi_key_expiredThe key passed the expiry date it was created with.
402 insufficient_creditaccount_out_of_creditThe account is out of AI credit, so no run was started. An owner, or an admin with billing access, adds credit in Account → Billing.
402 insufficient_creditkey_spend_cap_reachedThis key has used its monthly spend cap. An owner or admin can raise it in Account → API keys.
403 permissioninsufficient_scopeThe key does not hold the scope this endpoint needs. The message names it.
403 permissionaccount_suspendedThe account is suspended. Contact VeraGen support.
403 permissionaccount_closedThe account has been closed.
404 not_foundresource_not_foundNothing with that id in this account. An id that belongs to another account gives exactly this answer.
404 not_foundroute_not_foundNo endpoint at that path.
404 not_foundapi_not_availableThe API is not open on this service yet. Every request gets this answer until it is.
405 invalid_requestmethod_not_allowedThe path exists but not with that method. The Allow header lists the methods it takes.
409 conflictidempotency_in_progressA request with this Idempotency-Key is still being handled. Retry with the same key in a few seconds.
409 conflictidempotency_outcome_unknownThe first request with this Idempotency-Key did not finish (it timed out or failed part way), so whether it started the run or created the page is unknown. It is never run again: list the workflow's runs or the project's items to see whether it did, and use a new key if it did not.
409 conflictcontact_existsAnother contact in this account already has that email address. resource_id is its id: update it with PATCH instead.
409 conflictcontact_do_not_contactThe address belongs to someone who was erased from Thrive at their request. They stay on do-not-contact: the API cannot add them again or change them.
409 conflictopt_out_lockedThe contact has opted out of email. The API cannot opt them back in; a person can, in Thrive.
413 invalid_requestbody_too_largeThe body is larger than 1 MB.
413 invalid_requestpage_too_largeThe page would be larger than a page can be once converted to HTML. Split it into two pages.
415 invalid_requestunsupported_media_typeA body that is not application/json.
422 invalid_requestunknown_fieldThe body has a field this endpoint does not take. param names it. Unknown fields are refused, never ignored.
422 invalid_requestinvalid_bodyThe body is not a JSON object.
422 invalid_requestinvalid_fieldA body field has a value it cannot take. param names it.
422 invalid_requestworkflow_emptyThe workflow has no blocks, so there is nothing to run.
422 invalid_requestidempotency_key_reusedThis Idempotency-Key was already used for a different request. Use a new key for a new request.
429 rate_limitrate_limitedToo many requests for this key in the current minute. Wait for Retry-After seconds.
429 rate_limittoo_many_failed_authToo many requests with a wrong key from one address in a minute.
429 rate_limitrun_rate_limitedThe account has started as many runs through the API in the last minute as its plan allows.
429 rate_limitconcurrency_limitedThe account already has as many API runs queued or running as its plan allows. Start another when one finishes.
500 api_errorinternal_errorSomething went wrong on our side. Retry with backoff; quote the request_id to support.
cURL
curl -i "https://api.veragen.ai/v1/workflows?cursor=not-a-cursor" \
  -H "Authorization: Bearer $VERAGEN_API_KEY"
JavaScript
const res = await fetch("https://api.veragen.ai/v1/workflows?cursor=not-a-cursor", {
  headers: { Authorization: `Bearer ${process.env.VERAGEN_API_KEY}` },
});
if (!res.ok) {
  const { error } = await res.json();
  console.error(error.code, error.message, error.request_id);
}
Python
import os, requests

res = requests.get(
    "https://api.veragen.ai/v1/workflows",
    params={"cursor": "not-a-cursor"},
    headers={"Authorization": f"Bearer {os.environ['VERAGEN_API_KEY']}"},
)
if not res.ok:
    err = res.json()["error"]
    print(err["code"], err["message"], err["request_id"])
Response 400
{
  "error": {
    "type": "invalid_request",
    "code": "invalid_cursor",
    "message": "cursor is not valid",
    "param": "cursor",
    "request_id": "req_5f0c2a9e1b7d4c3a8e6f1d20"
  }
}

Pagination

Lists come a page at a time. Ask for up to 100 items with limit (the default is 25). Each page says whether there is more:

  • has_more: true when there is another page;
  • next_cursor: pass it back as cursor to get that page, with the same filters.

A cursor is opaque: use it as it is, and only with the endpoint and filters that gave it out. A cursor that has been changed is 400 invalid_cursor. Items that share a timestamp are never repeated or skipped across pages.

cURL
# First page
curl "https://api.veragen.ai/v1/workflows?limit=50" \
  -H "Authorization: Bearer $VERAGEN_API_KEY"

# Next page: the next_cursor from the previous answer
curl "https://api.veragen.ai/v1/workflows?limit=50&cursor=$NEXT_CURSOR" \
  -H "Authorization: Bearer $VERAGEN_API_KEY"
JavaScript
const all = [];
let cursor = null;
do {
  const url = new URL("https://api.veragen.ai/v1/workflows");
  url.searchParams.set("limit", "50");
  if (cursor) url.searchParams.set("cursor", cursor);
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.VERAGEN_API_KEY}` },
  });
  const page = await res.json();
  all.push(...page.data);
  cursor = page.next_cursor;
} while (cursor);
Python
import os, requests

headers = {"Authorization": f"Bearer {os.environ['VERAGEN_API_KEY']}"}
workflows, cursor = [], None
while True:
    params = {"limit": 50, **({"cursor": cursor} if cursor else {})}
    page = requests.get("https://api.veragen.ai/v1/workflows", params=params, headers=headers).json()
    workflows += page["data"]
    cursor = page["next_cursor"]
    if not cursor:
        break

Idempotency

A request that starts a workflow run must carry an Idempotency-Key header: a value you make up, unique to that one action. A UUID is ideal. It is what makes a retry safe. Creating a page or a contact takes the same header and works the same way, but there it is optional: without it, every request makes a new page (a contact is never made twice for one email address, key or no key). An update (PATCH) takes no key: sending the same change twice leaves the same result.

  • Retry with the same key and the same request, and you get the first answer again, marked Idempotent-Replay: true. Nothing is started twice.
  • The same key with a different request is refused with 422 idempotency_key_reused. Use a new key for a new action.
  • If the first request is still being handled, a retry gets 409 idempotency_in_progress. Wait a few seconds and retry with the same key.
  • If the first request did not finish (it timed out, or failed part way), whether it did anything cannot be known, so a retry gets 409 idempotency_outcome_unknown and nothing is run again. List the workflow's runs (or the project's items, or contacts with that email) to see whether it happened; if it did not, send it again with a new key.
  • A request refused before anything happened (no credit, an id that does not exist, a limit reached) does not use up its key: fix the cause and retry with the same key.

Keys are remembered for 24 hours.

cURL
curl -X POST "https://api.veragen.ai/v1/workflows/{id}/runs" \
  -H "Authorization: Bearer $VERAGEN_API_KEY" \
  -H "Idempotency-Key: 6f1d2c3b-run-2026-10-03-1420" \
  -H "Content-Type: application/json" \
  -d '{}'
JavaScript
const res = await fetch("https://api.veragen.ai/v1/workflows/{id}/runs", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.VERAGEN_API_KEY}`, "Idempotency-Key": "6f1d2c3b-run-2026-10-03-1420", "Content-Type": "application/json" },
  body: JSON.stringify({}),
});
const data = await res.json();
Python
import os, requests

res = requests.post(
    "https://api.veragen.ai/v1/workflows/{id}/runs",
    headers={"Authorization": f"Bearer {os.environ['VERAGEN_API_KEY']}", "Idempotency-Key": "6f1d2c3b-run-2026-10-03-1420"},
    json={},
)
data = res.json()
Response 202
{
  "id": "4b7e1c2d-3f5a-4b6c-8d7e-9f0a1b2c3d4e",
  "workflow_id": "3f6c1d2e-8a4b-4f7e-9c21-5d0e7a1b2c3d",
  "status": "queued",
  "trigger": "api",
  "created_at": "2026-10-03T14:20:00.000Z"
}

Rate limits

Each key may make 120 requests a minute. Every answer tells you where you stand:

  • RateLimit-Limit: requests allowed this minute;
  • RateLimit-Remaining: requests left this minute;
  • RateLimit-Reset: seconds until the count starts again.

Over the limit, the API answers 429 rate_limited with a Retry-After header: wait that many seconds, then carry on. Separately, more than 20 requests with a wrong key from one address in a minute are answered 429 too_many_failed_auth.

Starting runs has two more limits, per account: how many runs may be started through the API in a minute (429 run_rate_limited), and how many may be queued or running at once (429 concurrency_limited). Both answers carry Retry-After.

LimitSoloTeamBusinessEnterprise
Requests a minute, per key601203001,000
Workflow runs started a minute, per account61230By contract
Runs queued or running at once, per account2510By contract

Every account is on the Team limits today. The run limits count runs started through the API.

cURL
curl -i "https://api.veragen.ai/v1/workflows" \
  -H "Authorization: Bearer $VERAGEN_API_KEY"
# RateLimit-Limit: 120
# RateLimit-Remaining: 119
# RateLimit-Reset: 42
JavaScript
async function call(url) {
  for (;;) {
    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${process.env.VERAGEN_API_KEY}` },
    });
    if (res.status !== 429) return res;
    const wait = Number(res.headers.get("Retry-After") || 1);
    await new Promise((r) => setTimeout(r, wait * 1000));
  }
}
Python
import os, time, requests

def call(url, **kw):
    headers = {"Authorization": f"Bearer {os.environ['VERAGEN_API_KEY']}"}
    while True:
        res = requests.get(url, headers=headers, **kw)
        if res.status_code != 429:
            return res
        time.sleep(int(res.headers.get("Retry-After", "1")))
Response 429
{
  "error": {
    "type": "rate_limit",
    "code": "rate_limited",
    "message": "Too many requests for this API key. Try again in 12 seconds.",
    "request_id": "req_5f0c2a9e1b7d4c3a8e6f1d20"
  }
}

Versioning

The major version is in the path: /v1. Within v1, changes are only additions: new endpoints, new optional parameters and new fields in responses. Write your code to ignore fields it does not know.

A change that could break existing code gets a new version, /v2, and v1 keeps working for at least 12 months after it is announced. Anything due to be retired is listed in the changelog and announced with a Sunset header.

Timestamps are ISO 8601 in UTC and end in Z. Ids are UUIDs.