API and webhooks
Pull results into your scripts, export PDFs and re-run a monitor from CI, with the same key your agents use.
Which one to use
| Surface | Best for | Auth |
|---|---|---|
| MCP server | Agents in any MCP client | API key, Read & write |
| REST API | Scripts, dashboards and PDF exports | API key, Read only or Read & write |
| Webhook trigger | Starting an on-demand monitor from CI or a scheduler | The brand's webhook passkey |
There's no versioned reference or OpenAPI spec yet, and fields may be added. For agents, MCP is steadier.
Authentication
Create a key in Settings, API keys and send it as Authorization: Bearer sk_... or x-api-key: sk_.... Try it:
curl https://canoe.contentstack.com/api/projects \
--header "Authorization: Bearer $CONTENTSTACK_CANOE_API_KEY"
- Base URL
https://canoe.contentstack.com/api - Header
Beareris case sensitive, followed by the key once.- Read only
GETonly. Fine for dashboards and reports.- Read & write
- Anything the key's creator can do, including runs and brand profile edits.
- Format
- JSON. Most fields are snake_case, but some older ones are camelCase, like
urlId,jobIdandgeneratedAt.
Identity, limits and revoking are in How keys work.
Endpoints
A brand is a project. Each monitor has one id: results endpoints call it test_id, and monitor and run endpoints call it url_id (urlId in request bodies).
- BrandsList brands, 10 per page by default (use
skipandlimit), or read one. Each brand'surlslist carries its monitors'url_id.GET /api/projects GET /api/projects/ :id - Brand profileNames, competitors, products and personas
GET /api/projects/ :projectId/ brand-profile - Brand profile editsRead & writeAdd, change or remove competitors, products and personas. Products and
icpsuse the same paths as competitors.POST .../brand-profile/ competitors PATCH DELETE .../brand-profile/ competitors/ :id - MonitorsOne monitor's settings, questions and status
GET /api/test/ url/ :url_id - RunsRead & writeStart a run now. Spends prompts. Returns a
jobId.POST /api/test/ job/ force-run { "urlId": "..." } - HistoryRuns and trends, newest first, with each run's PDF export state.
history-runspages start at 0, andlimitis 1 to 50.GET /api/ai-section/ test/ :test_id/ history GET .../history-summary GET .../history-runs?page=0&limit=20 - ReportsThe full result of one run
GET /api/ai-section/ report/ :reportId - CitationsSources by owner and domain, or by page. Filter with
report_id,stage,fromandto.stageisAwareness,ConsiderationorDecision, exact case.GET /api/gso-citations/ summary?test_id=... GET /api/gso-citations/ by-url?test_id=... - PDF reportsGenerate needs Read & writeGenerate a run's PDF, check it and download it
POST /api/report/ :id/ pdf/ generate GET /api/report/ :id/ pdf/ status GET /api/report/ :id/ pdf
Example: export a PDF report
Take the report id from the history endpoint.
REPORT_ID="your_report_id"
AUTH="Authorization: Bearer $CONTENTSTACK_CANOE_API_KEY"
BASE="https://canoe.contentstack.com/api/report/$REPORT_ID/pdf"
# 1. Ask for the PDF
curl --request POST "$BASE/generate" --header "$AUTH"
# 2. Check until it's ready
curl "$BASE/status" --header "$AUTH"
# 3. Download it
curl --output report.pdf "$BASE" --header "$AUTH"
Webhook trigger
Start an on-demand monitor from a deploy or a nightly job. It uses the brand's passkey, not your API key.
Get the trigger URL
Set the monitor's schedule to On Demand. The Web hook option only appears on On Demand monitors.
Open the monitor's menu (the three dots) and choose Web hook.
Select Copy API Trigger URL, then copy the Secret Passkey (it starts with
whsec_and covers all the brand's on-demand monitors). Entry Key shows the header name to send it in. Store all three as CI secrets.
Call it
Send a GET with the passkey in the header that Entry Key names, spelled exactly as shown.
curl --request GET \
--url "https://canoe.contentstack.com/api/test/job/webhook-run/<webhook_run_id>" \
--header "$CONTENTSTACK_CANOE_WEBHOOK_HEADER: $CONTENTSTACK_CANOE_WEBHOOK_KEY"
- name: Re-measure AI visibility
run: |
curl --fail --request GET \
--url "$TRIGGER_URL" \
--header "$WEBHOOK_HEADER: $WEBHOOK_KEY"
env:
TRIGGER_URL: ${{ secrets.CONTENTSTACK_CANOE_TRIGGER_URL }}
WEBHOOK_HEADER: ${{ secrets.CONTENTSTACK_CANOE_WEBHOOK_HEADER }}
WEBHOOK_KEY: ${{ secrets.CONTENTSTACK_CANOE_WEBHOOK_KEY }}
- 200
- Queued. The reply is the monitor's record. Results in about two minutes.
- 401
- The passkey header is missing or wrong.
- 403
- Not enough prompts left for this run.
- Skipped
"skipped": truewith areasonofmissingordeletedmeans the monitor is gone.
Contentstack CMS webhooks send POST, so they can't call it directly. Use a relay, a CI step or trigger_geo_test_run over MCP.
Rate limits
- Monthly
- 10,000 API and MCP requests on Growth, shared by the workspace. Every call made with a key counts, including calls that return an error. Over MCP, only tool calls count. When they run out, calls get a
429until the reset. Custom on Enterprise (coming soon). - Bursts
- Limited per workspace. On a
429Too many requests, back off and retry. - Webhook
- Limited per IP address. Prompt limits apply.
- Prompts
- Reads are free. A run spends one prompt per question per engine. See Plans and limits.
Errors
401Key missing or not accepted
No key (No token provided), a revoked or mistyped key (Invalid API key) or a header that isn't Bearer plus one sk_ key, like Bearer twice (Invalid token).
What to do: check the header and the key.
403Read only key
A Read only key tried to change something.
What to do: use a Read & write key for POST, PATCH and DELETE.
403Workspace on Free
The key's workspace is on Free.
What to do: Upgrade to Growth.
403Out of prompts
Not enough prompts left to start a run.
What to do: wait for the monthly reset, or add a prompt pack on Growth.
404Id not in this workspace
The id isn't in this key's workspace. Some routes answer 404; others return {}, an empty runs list or zero counts.
What to do: list brands and monitors again and use those ids.
409Run already in progress
A run for this monitor is already in progress.
What to do: wait for it to finish.
429Too many requests
Too many requests in a short time.
What to do: back off and retry.
429Monthly requests used
The month's API and MCP requests are used. The reply has Retry-After and the reset date.
What to do: wait until the reset. Retrying sooner won't help.
Not available yet
- Outbound webhooksComing soonNothing calls you back when a run ends. Today, poll the history endpoint or
get_geo_latest_report. - Native alertsComing soonToday, run the Visibility alerts skill on your own schedule.
No SDK yet. Run reports export as PDF.