Developers

Connect Echo to anything. An API, webhooks, Zapier and n8n.

Read and change actions, leads, prompts and playbooks with an API key, get every event the moment it happens as a signed webhook, or skip the code with Echo's Zapier app and n8n node.

The basics

Every address below starts with https://echo-aeo.com/api/v1.

  • Keys

    Make a key in Echo under Settings → API and webhooks (the workspace owner can) and send it as “Authorization: Bearer echo_…”. A key works for one workspace, with the scopes you gave it; write includes read.

  • Limits

    120 requests a minute per key; after that Echo answers 429 with Retry-After. Plan limits (leads, playbook runs) apply as in the app.

  • Answers

    JSON with snake_case fields and ISO times. Lists are { data, next_cursor }: pass next_cursor as cursor for the next page. Errors are { error: { code, message } } with code unauthorized, forbidden, not_found, invalid_request, plan_limit, rate_limited or server_error.

  • Versions

    Everything is under /api/v1. Fields may be added; nothing is removed or renamed within v1.

curl https://echo-aeo.com/api/v1/actions?brand_id=YOUR_BRAND_ID \
  -H "Authorization: Bearer echo_YOUR_KEY"

The workspace

Who the key belongs to. Zapier and n8n use it to test a key.

GET/me

The key's workspace, its name and scopes, and the workspace's brands.

{ "workspace": { "id": "ws_…", "name": "Fernwick" }, "key": { "id": "key_…", "name": "Zapier", "scopes": ["leads:write"] },
  "brands": [{ "id": "b_x7…", "name": "Fernwick", "domain": "fernwick.example" }] }

GET/brands

The workspace's brands.

{ "data": [{ "id": "b_x7…", "name": "Fernwick", "domain": "fernwick.example", "created_at": "…" }], "next_cursor": null }

Actions

The to-do list every part of Echo feeds: what to change on the website, pages to build, where to get mentioned, follow-ups.

GET/actionsactions:read

A brand's actions, newest first.

brand_id · query, required
The brand's id (GET /brands).
status · query
todo, doing, done or dismissed.
limit · query
1–100, 25 by default.
cursor · query
next_cursor from the page before.
{ "data": [{
  "id": "Hh3kP9…", "brand_id": "b_x7…", "title": "Add a comparison page with Tallybird",
  "kind": "create_page", "impact": "high", "status": "todo", "source": "ai",
  "url": "https://echo-aeo.com/app/actions?brand=b_x7…&action=Hh3kP9…",
  "assignee_email": "priya@fernwick.example", "due_date": "2026-10-10",
  "detail": "…", "created_at": "2026-09-28T06:02:11.000Z", "done_at": null
}], "next_cursor": "eyJ0Ijo…" }

GET/actions/{id}actions:read

One action.

id · path, required
The action's id.
{
  "id": "Hh3kP9…", "brand_id": "b_x7…", "title": "Add a comparison page with Tallybird",
  "kind": "create_page", "impact": "high", "status": "todo", "source": "ai",
  "url": "https://echo-aeo.com/app/actions?brand=b_x7…&action=Hh3kP9…",
  "assignee_email": "priya@fernwick.example", "due_date": "2026-10-10",
  "detail": "…", "created_at": "2026-09-28T06:02:11.000Z", "done_at": null
}

POST/actionsactions:write

Adds a “Follow up” action to a brand's board.

brand_id · body, required
The brand's id.
title · body, required
Up to 180 characters.
detail · body
What to do, up to 2,000 characters.
impact · body
high, medium (default) or low.
due_date · body
YYYY-MM-DD.
assignee_email · body
A member of the workspace.
{
  "id": "Hh3kP9…", "brand_id": "b_x7…", "title": "Add a comparison page with Tallybird",
  "kind": "create_page", "impact": "high", "status": "todo", "source": "ai",
  "url": "https://echo-aeo.com/app/actions?brand=b_x7…&action=Hh3kP9…",
  "assignee_email": "priya@fernwick.example", "due_date": "2026-10-10",
  "detail": "…", "created_at": "2026-09-28T06:02:11.000Z", "done_at": null
}

PATCH/actions/{id}actions:write

Changes an action's status, due date or assignee. Marking it done sends action.done.

status · body
todo, doing, done or dismissed.
due_date · body
YYYY-MM-DD, or null to clear.
assignee_email · body
A member's email, or null to unassign.
{
  "id": "Hh3kP9…", "brand_id": "b_x7…", "title": "Add a comparison page with Tallybird",
  "kind": "create_page", "impact": "high", "status": "todo", "source": "ai",
  "url": "https://echo-aeo.com/app/actions?brand=b_x7…&action=Hh3kP9…",
  "assignee_email": "priya@fernwick.example", "due_date": "2026-10-10",
  "detail": "…", "created_at": "2026-09-28T06:02:11.000Z", "done_at": null
}

Leads

People who agreed to hear from the brand. Echo keeps only them, so adding one needs your word that they agreed.

GET/leadsleads:read

The workspace's leads, newest first.

email · query
Find one lead by email.
limit · query
1–100, 25 by default.
cursor · query
next_cursor from the page before.
{ "data": [{
  "id": "Lq81…", "email": "ines@quietharbour.example", "first_name": "Ines", "last_name": "Duarte",
  "company": "Quiet Harbour Studio", "website": "quietharbour.example", "source": "api",
  "can_email": true, "blocked_reason": null, "created_at": "2026-09-29T09:14:02.000Z"
}], "next_cursor": null }

GET/leads/{id}leads:read

One lead.

id · path, required
The lead's id.
{
  "id": "Lq81…", "email": "ines@quietharbour.example", "first_name": "Ines", "last_name": "Duarte",
  "company": "Quiet Harbour Studio", "website": "quietharbour.example", "source": "api",
  "can_email": true, "blocked_reason": null, "created_at": "2026-09-29T09:14:02.000Z"
}

POST/leadsleads:write

Adds a lead, or updates the one with that email (201 when new, 200 when updated).

email · body, required
Their email address.
consent · body, required
Your confirmation that they agreed to hear from you. Without it Echo refuses the lead.
first_name, last_name, company, website · body
What you know about them.
email_campaign_id · body
Adds them to an email campaign (GET /email-campaigns).
{
  "id": "Lq81…", "email": "ines@quietharbour.example", "first_name": "Ines", "last_name": "Duarte",
  "company": "Quiet Harbour Studio", "website": "quietharbour.example", "source": "api",
  "can_email": true, "blocked_reason": null, "created_at": "2026-09-29T09:14:02.000Z",
  "created": true, "added_to_campaign": null
}

GET/email-campaignsleads:read

The workspace's email campaigns, to add leads to.

{ "data": [{ "id": "c_…", "name": "AI score follow-up", "status": "active", "brand_id": "b_x7…", "created_at": "…" }], "next_cursor": null }

Prompts and AI visibility

The questions Echo asks AI assistants for a brand, and how often the answers name it.

GET/promptsprompts:read

A brand's prompts with their topic, intent, market and whether they're tracked.

brand_id · query, required
The brand's id (GET /brands).
{ "data": [{ "id": "p_…", "text": "Best CRM for a small agency?", "topic": "CRM for small teams", "intent": "commercial", "market_id": "m_…", "active": true, "created_at": "…" }], "next_cursor": null }

GET/metricsmetrics:read

AI visibility for a period: share of answers naming the brand, share of voice, position and tone, for the brand and each competitor, and per day.

brand_id · query, required
The brand's id (GET /brands).
from, to · query
YYYY-MM-DD; the last 30 days by default.
engine · query
all (default), chatgpt, perplexity, gemini, claude or google_aio.
market · query
A market's id.
{ "brand_id": "b_x7…", "from": "2026-08-31", "to": "2026-09-29", "engine": "all",
  "brand": { "visibility": 0.41, "share_of_voice": 0.22, "avg_position": 2.3, "positive_share": 0.64, "mentions": 412, "answers": 1004, "citations": 88 },
  "competitors": [{ "id": "…", "name": "Tallybird", "domain": "tallybird.example", "visibility": 0.58, … }],
  "days": [{ "date": "2026-09-29", "answers": 40, "visibility": 0.43 }] }

Playbooks and events

Playbooks run by themselves when something happens. Start one for a lead from your own tools, or read the events of the last 30 days.

GET/playbooksplaybooks:read

A brand's playbooks.

brand_id · query, required
The brand's id (GET /brands).
{ "data": [{ "id": "pb_…", "name": "Welcome new form leads", "trigger": "form.submitted", "enabled": true, "brand_id": "b_x7…", "created_at": "…" }], "next_cursor": null }

POST/playbooks/{id}/runsplaybooks:write

Runs a playbook about a person (form sign-ups, AI score sign-ups, replies) for one of your leads. Counts toward the plan's runs.

lead_id · body, required
The lead's id.
{ "id": "run_…", "playbook_id": "pb_…", "status": "done" }

GET/eventsplaybooks:read

Events of the last 30 days that a playbook or webhook listened to, newest first.

type · query
One event type (below).
limit · query
1–100, 25 by default.
sample · query
With type: a made-up event when there's none yet (for testing a Zap).
{ "data": [ { "id": "evt_…", "type": "form.submitted", … } ], "next_cursor": null }

Webhooks

Zapier and n8n subscribe here when a Zap or workflow with an Echo trigger is switched on, and unsubscribe when it's switched off. Your own code can too.

GET/webhookswebhooks:read

The workspace's webhooks.

{ "data": [{ "id": "wh_…", "url": "https://hooks.zapier.com/…", "events": ["form.submitted"], "brand_id": null, "enabled": true, … }], "next_cursor": null }

POST/webhookswebhooks:write

Adds a webhook. Its signing secret is in the answer, once.

url · body, required
A public https address.
events · body
Event types to send (or one as event).
brand_id · body
Only this brand's events.
{ "id": "wh_…", "url": "https://hooks.zapier.com/…", "events": ["lead.replied"], "enabled": true, "secret": "whsec_…", … }

DELETE/webhooks/{id}webhooks:write

Removes a webhook.

id · path, required
The webhook's id.
{ "id": "wh_…", "deleted": true }

Events

What playbooks start on and webhooks send. Each delivery is a POST with the event as JSON: its id, type, time, workspace, brand and data.

form.submitted

A lead form is submitted

When someone confirms their email after filling in one of the brand's lead forms (double opt-in), so they're an opted-in lead.

score.signup

Someone runs the free AI score

When someone runs the free AI score on the brand's website. They asked for a scan, not for emails, so email steps skip them unless they're already an opted-in lead.

lead.replied

A lead replies to an email campaign

When a lead answers a mail of an email campaign. Out-of-office replies don't count.

company.visited

A company views a page

When Company visitors sees a company view a page of the website, once per company, page and day.

account.signal

A target account shows a buying signal

When a target account shows a new buying signal: a visit, hiring, funding, a new leader, news, an event or a tech change.

action.created

An action is created

When Echo or a teammate adds an action to the board.

action.done

An action is done

When an action is marked done.

campaign.started

A campaign starts

When a campaign's start date arrives.

campaign.ended

A campaign ends

When a campaign's end date has passed.

{
  "id": "evt_4GJ2kq9V…",
  "type": "form.submitted",
  "created_at": "2026-09-29T09:14:02.000Z",
  "workspace_id": "ws_…",
  "brand": {
    "id": "b_x7…",
    "name": "Fernwick",
    "domain": "fernwick.example"
  },
  "data": {
    "form": {
      "id": "sample-form",
      "name": "Newsletter"
    },
    "lead": {
      "id": "sample-lead",
      "email": "jamie@sample.example",
      "first_name": "Jamie",
      "last_name": "Rivera",
      "company": "Sample Co",
      "website": "sample.example",
      "source": "form",
      "can_email": true
    },
    "fields": {
      "email": "jamie@sample.example",
      "first_name": "Jamie"
    }
  }
}

Signed deliveries

Every webhook has its own secret, shown once when it's made. Echo signs each delivery the Standard Webhooks way (headers webhook-id, webhook-timestamp and webhook-signature), so any Standard Webhooks library checks it too. A delivery that fails is tried 6 times over about 8 hours; an answer of 410 switches the webhook off.

import crypto from "node:crypto";

// secret: the webhook's "whsec_…"; headers and the raw body as received.
export function fromEcho(secret, headers, body) {
  const id = headers["webhook-id"], ts = headers["webhook-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // older than 5 minutes
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = "v1," + crypto.createHmac("sha256", key).update(`${id}.${ts}.${body}`).digest("base64");
  return headers["webhook-signature"].split(" ").some((s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}

Zapier and n8n

No code: connect Echo with an API key and pick what should happen.

  • ZapierTriggers for every event (a new form lead, a reply, a target account signal, an action done …) and actions to add a lead, add or update an action and run a playbook. Paste an API key with the scopes you want when Zapier asks.
  • n8nThe Echo node (actions, leads, playbooks, metrics) and the Echo Trigger node for every event. Install n8n-nodes-echo under Settings → Community nodes, then add your API key as a credential.

Missing something in the API? Tell us what you're building.

Write to hello@echo-aeo.com with what you'd like to read or change.