API reference
The ExpertRelay API
Your own systems can read and act on your workspace: your customers, sales, skills and what customers asked. Add clients from your CRM, put answers in your reports, post help requests to Slack.
Getting started
- In your workspace, open Settings → API and webhooks and make a key.
- Send it on every request as
Authorization: Bearer er_…. - Call any endpoint below. Requests and responses are JSON.
curl https://expertrelay.ai/api/v1/week \
-H "Authorization: Bearer $EXPERTRELAY_KEY"
Base URL: https://expertrelay.ai/api/v1/. Your workspace is the key’s workspace: an agency’s key reaches agency endpoints, an expert’s key reaches expert endpoints, and everyone has the shared ones. The machine-readable description is at openapi.json, for Postman, code generators and Zapier.
API keys
- Read only keys read everything and change nothing.
- Can make changes keys also act: add a client, reply to a help request, give customers a new version. They act straight away, with no approval step; your system has decided. Use them only where you’d trust the system with that.
- A key is shown once, when you make it. We keep only a hash of it. Revoke a key in Settings and it stops working at once; so do all of a workspace’s keys if its owner’s account is deactivated.
Responses and errors
Every response is JSON. Results include links into the app where it helps. When something can’t be done, the response has an error in plain words:
{"error": "No client matches “Acme”. list_clients shows them all."}
| Status | Means |
|---|---|
200 | Done, or here’s what you asked for. |
400 | Something to fix in the request; error says what. |
401 | No key, or the key isn’t valid or was revoked. |
403 | This key is read only, and the endpoint changes something. |
404 | No such endpoint for this workspace, or nothing matches (a client, a skill, a question). |
405 | Wrong method for this endpoint. |
413 | The body is too big (over 2.5 MB). Upload big skills on the website. |
429 | More than 120 requests a minute with this key. Wait a moment. |
500 | Something went wrong on our side. Try again in a minute. |
Clients, customers and skills can be named by email, name or id. Anything that changes a skill needs its full name, so a near miss never lands on another skill.
Everyone 20 endpoints
For agencies and experts alike.
/api/v1/needs-you
What needs you
What needs the expert right now: customers asking for help, setup left to do (Stripe, a price, skills to make live), people who have access but never used it, open questions. Start here.
curl https://expertrelay.ai/api/v1/needs-you \
-H "Authorization: Bearer $EXPERTRELAY_KEY"
/api/v1/week
How the week went
The last 7 days: revenue, refunds, new and paying customers, answers given, and how many have access or connected but haven’t used it.
curl https://expertrelay.ai/api/v1/week \
-H "Authorization: Bearer $EXPERTRELAY_KEY"
/api/v1/activity
What clients asked
Questions your clients or customers asked, newest first, each with the skill, the decision and whether it needs a look. Use get_answer with a run_id for the full answer.
| Parameter | In | Type | About |
|---|---|---|---|
client |
query | string | An agency’s client: name, email or id. |
customer |
query | string | An expert’s customer: email, name or id. |
skill |
query | string | Part of a skill’s name. |
since_days |
query | integer | How far back, in days (default 7). |
needs_a_look |
query | boolean | Only questions that need the agency. |
limit |
query | integer | At most this many rows (default 25). |
curl https://expertrelay.ai/api/v1/activity \
-H "Authorization: Bearer $EXPERTRELAY_KEY"
/api/v1/answers/{run_id}
One answer
One question in full: what the client asked, the whole answer, the data it used, what the agency said about it, and any help request with the agency’s reply.
| Parameter | In | Type | About |
|---|---|---|---|
run_id required |
path | string |
curl https://expertrelay.ai/api/v1/answers/RUN_ID \
-H "Authorization: Bearer $EXPERTRELAY_KEY"
/api/v1/answers/{run_id}/right
Needs a key that can make changes
Mark an answer right
Record that an answer a client got was right.
| Parameter | In | Type | About |
|---|---|---|---|
run_id required |
path | string | The question’s run_id, from list_activity. |
curl -X POST https://expertrelay.ai/api/v1/answers/RUN_ID/right \
-H "Authorization: Bearer $EXPERTRELAY_KEY"
/api/v1/answers/{run_id}/correction
Needs a key that can make changes
Teach the skill from an answer
Teach a skill what an answer should have said. We write the next version with it; clients keep the current one until make_version_available.
| Parameter | In | Type | About |
|---|---|---|---|
run_id required |
path | string | The question’s run_id, from list_activity. |
what_went_wrong required |
body | string | One of: missed_rule, wrong_question, wrong_facts, format, should_decline, other. |
should_have_said required |
body | string |
curl https://expertrelay.ai/api/v1/answers/RUN_ID/correction \
-H "Authorization: Bearer $EXPERTRELAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"what_went_wrong": "missed_rule",
"should_have_said": "Pause only after 7 days of data, not 3."
}'
/api/v1/help-requests/{run_id}/rerun
Needs a key that can make changes
Run a help request again
Run a client’s request that asked for help again, on the current version with their data now. Only the agency sees the answer (get_answer → help_request.rerun).
| Parameter | In | Type | About |
|---|---|---|---|
run_id required |
path | string | The question’s run_id, from list_activity. |
curl -X POST https://expertrelay.ai/api/v1/help-requests/RUN_ID/rerun \
-H "Authorization: Bearer $EXPERTRELAY_KEY"
/api/v1/help-requests/{run_id}/reply
Needs a key that can make changes
Reply to a help request
Email the client from the agency about their help request, optionally with the re-run answer, and mark it handled.
| Parameter | In | Type | About |
|---|---|---|---|
run_id required |
path | string | The question’s run_id, from list_activity. |
message required |
body | string | |
include_answer |
body | boolean |
curl https://expertrelay.ai/api/v1/help-requests/RUN_ID/reply \
-H "Authorization: Bearer $EXPERTRELAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "Thanks for flagging this. Here'"'"'s the full answer.",
"include_answer": true
}'
/api/v1/help-requests/{run_id}/handled
Needs a key that can make changes
Mark a help request handled
Close a help request handled another way, like a call. Sends nothing.
| Parameter | In | Type | About |
|---|---|---|---|
run_id required |
path | string | The question’s run_id, from list_activity. |
curl -X POST https://expertrelay.ai/api/v1/help-requests/RUN_ID/handled \
-H "Authorization: Bearer $EXPERTRELAY_KEY"
/api/v1/skills
Skills
Your skills: version, status, open questions and questions this week; for an agency who gets each and what client data it reads, for an expert its price and how many customers have it.
curl https://expertrelay.ai/api/v1/skills \
-H "Authorization: Bearer $EXPERTRELAY_KEY"
/api/v1/skills/{skill}/try
Needs a key that can make changes
Try a skill’s new version
Run a skill’s newest version: on the corrected question when it was made from a correction, otherwise on its sample. Only the agency sees the answer (get_try).
| Parameter | In | Type | About |
|---|---|---|---|
skill required |
path | string | The skill’s name. |
curl -X POST https://expertrelay.ai/api/v1/skills/Weekly%20ads%20triage/try \
-H "Authorization: Bearer $EXPERTRELAY_KEY"
/api/v1/skills/{skill}/release
Needs a key that can make changes
Give clients the new version
Make a skill’s newest version the one clients use.
| Parameter | In | Type | About |
|---|---|---|---|
skill required |
path | string | The skill’s name. |
curl -X POST https://expertrelay.ai/api/v1/skills/Weekly%20ads%20triage/release \
-H "Authorization: Bearer $EXPERTRELAY_KEY"
/api/v1/tries/{run_id}
A try’s answer
The state and answer of a try started with try_new_version.
| Parameter | In | Type | About |
|---|---|---|---|
run_id required |
path | string |
curl https://expertrelay.ai/api/v1/tries/RUN_ID \
-H "Authorization: Bearer $EXPERTRELAY_KEY"
/api/v1/skills/from-session
Needs a key that can make changes
Make a skill from this session
Use when the owner has just worked out how they decide or do something in this conversation (a price, a triage, a review, a reply), corrected you on how they do it, or asks you to remember it or make it a skill: offer to turn it into one of their skills, as a private draft only they can use. Call list_skills first; if one of theirs already does this job, use add_session_to_skill instead, so one job stays one skill. Prefer a packet: the job, the method step by step, the rules and thresholds they used, every place they corrected you (corrections), worked examples from the session (situation, what they had, what they decided, why), and what isn’t settled; we write the skill from it and ask open questions. If they already have a finished skill, send it instead as skill_md (the SKILL.md text), files (a skill folder as {path, content}), or zip_base64; its description must say when to use it, in a sentence starting “Use when”.
| Parameter | In | Type | About |
|---|---|---|---|
packet |
body | object | |
skill_md |
body | string | A finished SKILL.md, with a name and a description that says when to use it (“Use when …”). |
files |
body | array of objects | A finished skill folder, including SKILL.md. |
zip_base64 |
body | string | A finished skill as a ZIP, base64, under 8 MB. |
source |
body | string | Where the session was, e.g. Claude or ChatGPT. |
personal_details_removed required |
body | boolean |
curl https://expertrelay.ai/api/v1/skills/from-session \
-H "Authorization: Bearer $EXPERTRELAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"personal_details_removed": true,
"source": "Claude",
"packet": {
"title": "Weekly ads triage",
"job": "Decide which ad sets to pause, scale or leave each week.",
"method": [
"Pull the last 14 days per ad set.",
"Compare cost per lead with the target."
],
"rules": [
"Cost per lead over 2x target with 3+ days of data: pause."
],
"examples": [
{
"situation": "[client], Broad ad set",
"inputs": "CPL $84 vs $40 target, 6 days",
"decision": "Pause Broad",
"why": "Over twice the target with enough data."
}
],
"unsure": [
"What to do when spend is under $500."
]
}
}'
/api/v1/skills/{skill}/status
Where a skill stands
Whether a skill from teach_from_session is still being written, ready to try (with its open questions), or what went wrong. By its job_id, or by the skill’s name.
| Parameter | In | Type | About |
|---|---|---|---|
skill required |
path | string | The skill’s name. |
job_id |
query | string |
curl https://expertrelay.ai/api/v1/skills/Weekly%20ads%20triage/status \
-H "Authorization: Bearer $EXPERTRELAY_KEY"
/api/v1/teach-jobs/{job_id}
Where a skill stands
Whether a skill from teach_from_session is still being written, ready to try (with its open questions), or what went wrong. By its job_id, or by the skill’s name.
| Parameter | In | Type | About |
|---|---|---|---|
job_id required |
path | string | |
skill |
query | string | The skill’s name. |
curl https://expertrelay.ai/api/v1/teach-jobs/JOB_ID \
-H "Authorization: Bearer $EXPERTRELAY_KEY"
/api/v1/skills/{skill}/answers
Needs a key that can make changes
Answer a skill’s open questions
We write the skill’s next version with them, privately; answers are kept for later versions too.
| Parameter | In | Type | About |
|---|---|---|---|
skill required |
path | string | The skill’s name. |
answers required |
body | array of objects | |
note |
body | string | Anything else that changed. |
curl https://expertrelay.ai/api/v1/skills/Weekly%20ads%20triage/answers \
-H "Authorization: Bearer $EXPERTRELAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"answers": [
{
"number": 1,
"answer": "Wait two weeks of data before pausing."
}
]
}'
/api/v1/skills/{skill}/sessions
Needs a key that can make changes
Add this session to a skill
Use when this conversation showed something one of the owner’s existing skills should know: a new case, an edge case, a rule they hadn’t said, or a correction to an answer the skill gave. list_skills names their skills. Send a packet, with what it changes from what the skill does now in changes (we add it to what the skill was written from), or, for a skill uploaded as finished, the updated skill as skill_md, files or zip_base64. Makes its next version, privately.
| Parameter | In | Type | About |
|---|---|---|---|
skill required |
path | string | The skill’s name. |
packet |
body | object | |
skill_md |
body | string | A finished SKILL.md, with a name and a description that says when to use it (“Use when …”). |
files |
body | array of objects | A finished skill folder, including SKILL.md. |
zip_base64 |
body | string | A finished skill as a ZIP, base64, under 8 MB. |
personal_details_removed required |
body | boolean |
curl https://expertrelay.ai/api/v1/skills/Weekly%20ads%20triage/sessions \
-H "Authorization: Bearer $EXPERTRELAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"personal_details_removed": true,
"packet": {
"title": "A seasonal edge case",
"job": "Decide which ad sets to pause, scale or leave each week.",
"examples": [
{
"situation": "[client] with a seasonal spike",
"decision": "Don'"'"'t pause during the spike week",
"why": "Cost per lead always doubles that week and recovers."
}
]
}
}'
/api/v1/skills/{skill}/export
A skill as SKILL.md
One of the owner’s skills as a Claude skill: its SKILL.md and other text files, to read, keep or use elsewhere, with a download link for the whole folder.
| Parameter | In | Type | About |
|---|---|---|---|
skill required |
path | string | The skill’s name. |
curl https://expertrelay.ai/api/v1/skills/Weekly%20ads%20triage/export \
-H "Authorization: Bearer $EXPERTRELAY_KEY"
/api/v1/upload-link
A link for big files
When files are too big to send through the chat (or the AI can’t read their bytes), a link the owner opens in their browser to drop them: a finished skill ZIP, material to write a new skill from, or files to add to an existing skill.
| Parameter | In | Type | About |
|---|---|---|---|
what |
query | string | One of: material, finished_skill. |
skill |
query | string | The skill’s name. |
curl https://expertrelay.ai/api/v1/upload-link \
-H "Authorization: Bearer $EXPERTRELAY_KEY"
Experts 7 endpoints
Only for expert (creator) workspaces.
/api/v1/customers
Customers
Everyone with access to your skills: their skills, how far they got (has access, connected, used it), answers, and when they last asked.
| Parameter | In | Type | About |
|---|---|---|---|
stage |
query | string | One of: has access, not used, connected, not used, used it. |
skill |
query | string | The skill’s name. |
query |
query | string | Part of a name or email. |
limit |
query | integer | At most this many rows (default 25). |
curl https://expertrelay.ai/api/v1/customers \
-H "Authorization: Bearer $EXPERTRELAY_KEY"
/api/v1/customers/{customer}
One customer
One customer in full: their access and how they got it, purchases, and last 10 questions.
| Parameter | In | Type | About |
|---|---|---|---|
customer required |
path | string | The customer’s email, name or id. |
curl https://expertrelay.ai/api/v1/customers/dana%40example.com \
-H "Authorization: Bearer $EXPERTRELAY_KEY"
/api/v1/customers/{customer}/remind
Needs a key that can make changes
Remind customers to use it
Email a reminder with their sign-in link: to one customer, or to everyone who has access but hasn’t used it (who = claimed-not-run) or connected but hasn’t (connected-not-run).
| Parameter | In | Type | About |
|---|---|---|---|
customer required |
path | string | The customer’s email, name or id. |
who |
body | string | One of: claimed-not-run, connected-not-run. |
skill |
body | string | The skill’s name. |
curl https://expertrelay.ai/api/v1/customers/dana%40example.com/remind \
-H "Authorization: Bearer $EXPERTRELAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"who": "claimed-not-run"
}'
/api/v1/reminders
Needs a key that can make changes
Remind customers to use it
Email a reminder with their sign-in link: to one customer, or to everyone who has access but hasn’t used it (who = claimed-not-run) or connected but hasn’t (connected-not-run).
| Parameter | In | Type | About |
|---|---|---|---|
customer |
body | string | The customer’s email, name or id. |
who |
body | string | One of: claimed-not-run, connected-not-run. |
skill |
body | string | The skill’s name. |
curl https://expertrelay.ai/api/v1/reminders \
-H "Authorization: Bearer $EXPERTRELAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"who": "claimed-not-run"
}'
/api/v1/access
Needs a key that can make changes
Give someone a skill
Give someone a skill for free by email, and email them a sign-in link.
| Parameter | In | Type | About |
|---|---|---|---|
email required |
body | string | |
name |
body | string | |
skill required |
body | string | The skill’s name. |
send_email |
body | boolean | Default true. |
curl https://expertrelay.ai/api/v1/access \
-H "Authorization: Bearer $EXPERTRELAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"skill": "Offer review",
"send_email": true
}'
/api/v1/sales
Sales
Purchases and subscriptions, newest first, with the total paid.
| Parameter | In | Type | About |
|---|---|---|---|
since_days |
query | integer | How far back, in days (default 30). |
limit |
query | integer | At most this many rows (default 25). |
curl https://expertrelay.ai/api/v1/sales \
-H "Authorization: Bearer $EXPERTRELAY_KEY"
/api/v1/skills/{skill}/price
Needs a key that can make changes
Change a skill’s price
Set a skill’s price: free, one_time, monthly or yearly, with an amount in your currency. People who already have it keep it.
| Parameter | In | Type | About |
|---|---|---|---|
skill required |
path | string | The skill’s name. |
pricing required |
body | string | One of: free, one_time, monthly, yearly. |
amount |
body | number |
curl -X PUT https://expertrelay.ai/api/v1/skills/Weekly%20ads%20triage/price \
-H "Authorization: Bearer $EXPERTRELAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"pricing": "monthly",
"amount": 19
}'
Webhooks
Add a webhook in Settings → API and webhooks: an https address and the events you want. We send each event as a POST with a JSON body, signed with the webhook’s secret.
| Event | When | For |
|---|---|---|
customer.asked | A customer’s question was answered, or didn’t finish | Experts |
customer.added | Someone got one of your skills: bought, claimed or given | Experts |
purchase.completed | Someone paid | Experts |
help_request.opened | A customer asked for help | Experts |
help_request.handled | You replied to a help request, or marked it handled | Experts |
version.released | Customers got a skill’s new version | Experts |
What arrives
Headers: X-Expertrelay-Event, X-Expertrelay-Delivery (an id that stays the same across retries) and X-Expertrelay-Signature. The body:
customer.asked · A customer’s question was answered, or didn’t finish
{
"id": "DELIVERY_ID",
"event": "customer.asked",
"created_at": "2026-10-07T09:00:00+00:00",
"data": {
"run_id": "RUN_ID",
"customer": {
"id": "…",
"name": "Dana",
"email": "[email protected]"
},
"skill": "Offer review",
"question": "Is my price too low?",
"decision": "Raise it to $49.",
"result": "Answered",
"answer": "…",
"link": "…/studio/your-page/customers/…/"
}
}
customer.added · Someone got one of your skills: bought, claimed or given
{
"id": "DELIVERY_ID",
"event": "customer.added",
"created_at": "2026-10-07T09:00:00+00:00",
"data": {
"customer": {
"id": "…",
"name": "Sam",
"email": "[email protected]"
},
"skill": "Offer review",
"how": "stripe_purchase"
}
}
purchase.completed · Someone paid
{
"id": "DELIVERY_ID",
"event": "purchase.completed",
"created_at": "2026-10-07T09:00:00+00:00",
"data": {
"purchase_id": "…",
"customer": {
"id": "…",
"name": "Sam",
"email": "[email protected]"
},
"skill": "Offer review",
"amount": 29.0,
"currency": "usd",
"pricing": "one_time",
"interval": null
}
}
help_request.opened · A customer asked for help
{
"id": "DELIVERY_ID",
"event": "help_request.opened",
"created_at": "2026-10-07T09:00:00+00:00",
"data": {
"run_id": "RUN_ID",
"client": {
"id": "…",
"name": "Acme Dental",
"email": "[email protected]"
},
"they_wrote": "It stopped halfway through my report.",
"link": "…"
}
}
help_request.handled · You replied to a help request, or marked it handled
{
"id": "DELIVERY_ID",
"event": "help_request.handled",
"created_at": "2026-10-07T09:00:00+00:00",
"data": {
"run_id": "RUN_ID",
"client": {
"id": "…",
"name": "Acme Dental",
"email": "[email protected]"
},
"they_wrote": "It stopped halfway through my report.",
"your_reply": "Sorry about that. Here's the full answer.",
"link": "…"
}
}
version.released · Customers got a skill’s new version
{
"id": "DELIVERY_ID",
"event": "version.released",
"created_at": "2026-10-07T09:00:00+00:00",
"data": {
"skill": "Weekly ads triage",
"version": 3
}
}
Checking a delivery is from us
X-Expertrelay-Signature: t=<unix time>,v1=<signature>. The signature is the hex HMAC-SHA256 of t. followed by the raw body, with your webhook’s secret. Compare in constant time, and reject a delivery whose t is more than 5 minutes off, so an old delivery can’t be replayed.
# Python
import hashlib, hmac, time
def from_ExpertRelay(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:
parts = dict(item.split("=", 1) for item in header.split(","))
sent_at = int(parts["t"])
if abs(time.time() - sent_at) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{sent_at}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
// Node.js
const crypto = require("crypto");
function fromExpertrelay(secret, rawBody, header, tolerance = 300) {
const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
const sentAt = Number(parts.t);
if (Math.abs(Date.now() / 1000 - sentAt) > tolerance) return false;
const expected = crypto.createHmac("sha256", secret).update(`${sentAt}.`).update(rawBody).digest("hex");
return expected.length === parts.v1.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
Retries
- Answer any
2xxwithin 5 seconds and the delivery counts as done. We don’t read the response body. - Otherwise we try again after 1 minutes, 5 minutes, 30 minutes, 2 hours, 12 hours, then give up. A webhook that keeps failing shows Failing since in Settings.
- A retry keeps the same
X-Expertrelay-Deliveryid: if you’ve handled it already, answer200and skip it. - We only send to public https addresses, never follow redirects, and keep delivery records for 30 days. Send a test in Settings delivers a
pingevent straight away.
Your own AI
Prefer to ask than to code? Connect Claude or ChatGPT to your workspace in Settings → Your AI. It reads the same things as the API, and with changes allowed it can act. Anything a client or customer would see waits for you to approve it in the app. At the end of a good session, ask it to turn the session into a skill.