Developers
API and MCP
Your visibility numbers, readable from anywhere. There is no Slack app, no Notion app and no Linear app, because the agent you already run reaches all three and only needs the data. Part of Growth and Agency.
A key
Settings, API and MCP, create a key. It is shown once and only its hash is stored, so if you lose it, revoke it and make another. Keys expire after a year and can be revoked at any time, which takes effect on the next request.
curl https://aureoapp.ai/api/v1/me \ -H "Authorization: Bearer aureo_your_key"
Every success is { "data": ... } and every failure is { "error": { "code", "message" } }, so one unwrapping function covers the whole API.
Endpoints
- GET/api/v1/meThe key, its scopes, and the brands it can read.
- GET/api/v1/brandsThose brands on their own.
- GET/api/v1/brands/{id}Everything about one brand in one call: metrics, gaps, legibility, traffic and cited pages.
MCP
Add it as an HTTP MCP server with your key as a bearer token. It works with Claude, Cursor and anything else that speaks the protocol.
https://aureoapp.ai/api/mcp
- list_brandsThe brands in this workspace. Call it first: every other tool needs an id.
- get_visibilityScore with its weights, mention rate, share of voice, position, citations, and the weekly history.
- get_gapsThe queries where a competitor gets through and you do not, per engine.
- get_legibilityWhether an assistant can read the site, and every failing check with its fix.
- get_trafficHumans, visits credited to the assistant that sent them, and crawler requests.
- get_cited_pagesThe pages assistants cite, how often, whether they still answer, and what kind they are.
A brand id is always checked against the workspace your key belongs to, so an id from somewhere else returns nothing rather than somebody else's week.
In Claude, in one command
claude mcp add --transport http aureo https://aureoapp.ai/api/mcp --header "Authorization: Bearer aureo_your_key"
Then ask in your own words: "which client slipped this week", "list the gaps for Zrno", "is anything we cite down". In claude.ai or Claude Desktop, add the same URL as a custom connector with the key as the bearer token. Cursor and anything else that speaks MCP over HTTP works the same way.
Errors
- 401 unauthorizedNo key, or a key that is not valid. Revoked, expired and unknown all answer the same way.
- 403 plan_requiredThe key is fine and the plan does not include the API.
- 404 not_foundNo brand by that id in this workspace. A key never carries a brand.
Webhooks
Add an https endpoint in Settings and we post to it when a scan finishes. Three attempts, one second apart, and every attempt is recorded whether it succeeded or not. A 4xx is taken as your answer and not retried; a 5xx or a timeout is.
Each delivery carries three headers: x-aureo-delivery, x-aureo-timestamp and x-aureo-signature. Sign deliveryId.timestamp.rawBody with HMAC SHA-256 and your signing secret, compare it to the value after v1=, reject anything older than a few minutes, and deduplicate on the delivery id.
const expected =
'v1=' + createHmac('sha256', secret)
.update(`${deliveryId}.${timestamp}.${rawBody}`)
.digest('hex');The delivery id and the timestamp are inside the signature on purpose. Signing the body alone would let anybody who captures one delivery replay it for ever, and you would have no way to tell.
Playbook: scan results in Slack
Aureo posts to your endpoint when a scan finishes; Slack accepts simple posts on an incoming webhook. A relay of thirty lines connects the two and verifies our signature on the way through, so nothing unsigned can write into your channel. Runs as a Cloudflare Worker on the free tier; any serverless function works the same.
1. In Slack: create an incoming webhook for your channel (Slack app settings, Incoming Webhooks). 2. Deploy the worker below with two variables: AUREO_SECRET (the signing secret from Settings, API and MCP) and SLACK_WEBHOOK_URL. 3. In Aureo Settings, add the worker's URL as a webhook for scan.completed.
export default {
async fetch(request, env) {
const body = await request.text();
const id = request.headers.get('x-aureo-delivery');
const ts = request.headers.get('x-aureo-timestamp');
const sig = request.headers.get('x-aureo-signature');
const key = await crypto.subtle.importKey(
'raw', new TextEncoder().encode(env.AUREO_SECRET),
{ name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
);
const mac = [...new Uint8Array(await crypto.subtle.sign(
'HMAC', key, new TextEncoder().encode(`${id}.${ts}.${body}`),
))].map((b) => b.toString(16).padStart(2, '0')).join('');
if (sig !== `v1=${mac}`) return new Response('bad signature', { status: 401 });
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300)
return new Response('too old', { status: 401 });
const { event, data } = JSON.parse(body);
if (event === 'scan.completed') {
const engines = (data.enginesOk ?? []).join(', ');
const failed = (data.enginesFailed ?? []).length
? ` (${data.enginesFailed.join(', ')} unavailable)`
: '';
await fetch(env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
text: `Aureo: weekly scan finished on ${engines}${failed}. ` +
`${data.recommendations ?? 0} new recommendations, ` +
`${data.citedPagesDown ?? 0} cited pages down.`,
}),
});
}
return new Response('ok');
},
};The payload for scan.completed carries brandId, scanCycleId, enginesOk, enginesFailed, recommendations and citedPagesDown. Answer 200 quickly and do the talking to Slack after checking the signature; a 5xx or a timeout is retried, a 4xx is taken as your answer.