Skip to content
Help and docsEcosystem API

zvena Ecosystem API

One graph of your workspace for any software: documents, issues, projects, events and designs, the links between them, and objects of your other apps — searchable together, linked both ways, and announced as they change.

What it is

Everything in zvena has an address, a ref: note:<id> for a page or a database row, issue:<id>, project:<id>, design:<file>[/<frame>], event:<id>, user:<id>. zvena keeps the links between them — a mention, a pasted link, a live block, a relation, a conversion, a link made by hand — in both directions. The Ecosystem API opens that graph to your code, and lets your own apps put their objects into it: a pull request, a deal, a support ticket, a build becomes ext:<id>, with a card, a place in search, and links to the issues and documents it is about.

The Docs API (Notion-compatible pages, databases and blocks) stays what it is. The Ecosystem API works across products, and uses the same integrations and secrets.

Plans

The Ecosystem API is part of zvena Pro. On another plan every request answers 402 plan_required. When a workspace leaves Pro nothing is deleted: objects your apps added keep their cards and links in zvena; the API, the event feed and webhooks pause until the plan runs again. Self-hosted servers without billing have it for every workspace.

Getting started

  1. Settings → API → “New integration”. Copy the secret: it is shown once.
  2. Choose what it may do (scopes) and which products it reaches. Mail is personal and never reachable.
  3. Send the secret from your server in the Authorization header.
curl https://zvena.app/api/v1/eco/me \
  -H "Authorization: Bearer zv_secret_…"

An integration acts as the person who made it. It never sees more than they do — a private team's issue, a page not shared with them, a calendar they cannot read is simply not there — and it stops working if they leave the workspace.

Scopes

  • read — Search, cards, related objects and links of the products it was given. Always on.
  • write — Link objects by hand, file issues, change them and move them between statuses.
  • objects — Keep its own app's objects in zvena (external objects).
  • events — Read the event feed and receive webhooks.
  • products — any of docs, projects, calendar, design. Refs of other products are left out of every answer.

Endpoints

Base URL https://zvena.app/api. The full description, with every shape, is at /api/v1/eco/openapi.json (OpenAPI 3.1).

GET/v1/eco/meThe integration, its workspace, plan and scopesread
GET/v1/eco/searchSearch every product and other apps' objects at onceread
POST/v1/eco/cardsCards for refs (the ones the maker may see)read
POST/v1/eco/resolveWhat links name: zvena links, KEY-12, refs, and URLs of other apps' objectsread
GET/v1/eco/relatedEverything an object is linked with, both waysread
GET/v1/eco/linksAn object's links as storedread
POST/v1/eco/linksLink two objects by handwrite
DELETE/v1/eco/linksRemove a link made by handwrite
GET/v1/eco/teamsTeams issues can be filed inread
GET/v1/eco/projectsProjectsread
GET/v1/eco/issuesA project's issues, or the maker's ownread
POST/v1/eco/issuesFile an issuewrite
PATCH/v1/eco/issues/{id}Change an issue (status, title, priority, assignee, due date, project)write
GET/v1/eco/issues/{id}/statusesThe statuses an issue can move toread
GET/v1/eco/objectsThis integration's objects, newest change firstobjects
PUT/v1/eco/objects/{external_id}Create or replace an object of your appobjects
GET/v1/eco/objects/{external_id}One object of your appobjects
DELETE/v1/eco/objects/{external_id}Delete an object (its links go too)objects
POST/v1/eco/objects/batchCreate or replace up to 100 objectsobjects
GET/v1/eco/eventsThe event feed (poll with a cursor)events

Search and the graph

GET https://zvena.app/api/v1/eco/search?q=onboarding&kinds=issue,note,ext

GET https://zvena.app/api/v1/eco/related?ref=issue:8f2c…
{
  "object": "list",
  "ref": "issue:8f2c…",
  "results": [
    { "object": "related", "ref": "note:41ab…", "via": "embed", "direction": "in", "manual": false,
      "card": { "kind": "note", "title": "Onboarding spec", "url": "/docs?ws=…#…", … } },
    { "object": "related", "ref": "ext:Qm9…", "via": "mention", "direction": "in", "manual": false,
      "card": { "kind": "ext", "title": "Add onboarding emails", "sub": "GitHub · Open", "source": "GitHub", … } }
  ]
}

POST https://zvena.app/api/v1/eco/resolve
{ "urls": ["ENG-12", "https://zvena.app/projects/issue/ENG-12", "https://github.com/acme/app/pull/42"] }

A card is what a person sees of any object, whatever product it comes from: title, where it opens, a status with its category (backlog, unstarted, started, completed, canceled), assignee, due date, icon. resolve turns any link people paste — zvena's own, a KEY-12, or the URL of one of your objects — into a ref.

Your apps' objects

PUT an object by your own id; PUT again to replace it (unchanged input changes nothing). It shows to the workspace's members (not guests) in search and in the Related panel of everything it links to, and opens at its own URL.

PUT https://zvena.app/api/v1/eco/objects/pr%2F42
{
  "title": "Add onboarding emails",
  "type": "github.pull_request",
  "url": "https://github.com/acme/app/pull/42",
  "icon": "🔀",
  "status": { "name": "Open", "category": "started" },
  "assignee": "Maya",
  "properties": { "repo": "acme/app", "additions": 412, "draft": false },
  "text": "Closes ENG-12. Spec: https://zvena.app/docs#onboarding-41ab…",
  "links": ["project:5d1e…"]
}
  • links: refs, zvena links, KEY-12 or URLs of other objects — kept as sent (via relation).
  • text: searchable; the KEY-12, @mentions and zvena links inside it become links too (via mention, link).
  • Everything is resolved as the integration's maker: a key of a team they cannot see links nothing.
  • A link to the object's URL pasted anywhere in zvena resolves to it, however the URL is written (case, trailing slash, utm_ parameters).
  • POST /v1/eco/objects/batch takes up to 100 objects; a bad one gets its own error and the rest are saved. Revoking the integration removes its objects.
  • Limits: title 300 characters, text 20,000, 30 properties, 100 links, 50,000 objects per integration.

Issues

GET   https://zvena.app/api/v1/eco/teams
POST  https://zvena.app/api/v1/eco/issues        { "team_id": "…", "title": "Flaky checkout test", "priority": 2, "links": ["ext:Qm9…"] }
GET   https://zvena.app/api/v1/eco/issues/{id}/statuses
PATCH https://zvena.app/api/v1/eco/issues/{id}   { "status_id": "…" }

Events and webhooks

Events name refs, never content: read what you need through the API, with the same rules. An integration hears only about objects its maker may see, of the products it was given, and never about its own objects' writes. The feed keeps 7 days.

Types: document.changed, issue.changed, issue.commented, issue.deleted, project.changed, project.deleted, event.changed, design.changed, links.changed, object.changed, object.deleted.

GET https://zvena.app/api/v1/eco/events                 → { "results": [], "next_cursor": "1842" }   (starts now)
GET https://zvena.app/api/v1/eco/events?after=1842      → the events since, oldest first

{
  "object": "event", "id": "evt_1843", "seq": 1843,
  "type": "issue.changed", "timestamp": "2026-10-22T12:00:00.000Z",
  "workspace_id": "…", "integration_id": "…",
  "refs": ["issue:8f2c…"],
  "actor": { "id": "…", "type": "person" },
  "data": {}
}

With a webhook set in Settings → API, the same events are POSTed to your https endpoint, signed: X-Zvena-Signature: sha256=<HMAC-SHA256 of the body with the signing secret>. Any 2xx acknowledges; failures are retried after 1 s, 10 s and 1 min; private addresses are never called. Use the feed to catch up on anything a webhook missed.

// Node: check a delivery
import { createHmac, timingSafeEqual } from "node:crypto";
const expected = "sha256=" + createHmac("sha256", process.env.ZVENA_SIGNING_SECRET).update(rawBody).digest("hex");
const ok = timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers["x-zvena-signature"] ?? ""));

Plug it in anywhere

  • Any language: generate a client from the OpenAPI description (openapi-generator, oapi-codegen, openapi-typescript).
  • No-code: import the OpenAPI URL as a custom app or HTTP request in Zapier, Make or n8n; use the event feed as a polling trigger, or a webhook trigger.
  • AI agents: hand the OpenAPI description to an agent framework as tools; the integration's scopes and its maker's access bound what it can do.
  • Your own apps: sync objects with PUT on every change in your app, link them with text people already write (“Fixes ENG-12”), and react to zvena with events.

Rules

  • JSON only, bodies up to 2 MB. Bearer token from a server: browsers get no CORS grant.
  • About 10 requests a second per integration, bursts up to 50; over it, 429 with Retry-After.
  • Lists: page_size up to 100, start_cursor / after, next_cursor, has_more.
  • Errors: { object: "error", status, code, message } — unauthorized (401), plan_required (402), restricted_resource (403), object_not_found (404), validation_error and invalid_json (400), conflict_error (409), rate_limited (429).
  • Changes made through the API carry the maker's name in the history; links your objects make carry the integration's.

Free for everyone. No credit card, no trial, no ads.