Enrolment & messaging
Reach the people you already know but who have not joined yet, then talk to them. Invitations go out by email in the invitee's language with your branding and a one-time code; a phone-only invitation returns its join link once for you to deliver through your own consented channel (Norvya texts no one who has not opted in). Reusable join codes for a front desk or a class. Joining is always the person's own choice on the app's normal consent screen: an invitation never creates a link or sets a sharing switch, and the answer never reveals whether an address belongs to a Norvya account. Messages to a member land in their in-app inbox; the notification is generic and the text never reaches a lock screen. Refused when the member turned messages off for you; daily caps per member and per organisation. Bulk enrolment from a roster is the Enrolment roster family below. Webhooks: member.joined, message.received.
- Who calls it
- Every partner: common to all markets.
- Before production
- No gate of its own; your market's “Before production” applies.
- Sandbox
- Build against synthetic members first. What each sandbox holds
Markets that use this family
- Concierge doctors (core)
- Independent practices (core)
- Fitness chains (core)
- EMR vendors (useful)
- Federal (via primes) (useful)
- Insurers (incl. Medicaid managed care) (useful)
- Hospitals and health systems (core)
- Practice management & billing (useful)
- Medical travel desks (core)
- Pharmacies (useful)
- Nursing homes & assisted living (useful)
- Employers & occupational health (useful)
- Behavioral health practices (core)
- Rehab & nutrition practices (core)
- Older adults & caregiving programmes (core)
- Hospice agencies (useful)
- Med spas (useful)
Scopes
| Scope | Allows |
|---|---|
members.invite | Invite people to join your organisation on Norvya (Norvya emails its own invitation in the person's language; never a pre-granted consent: the person chooses what to share in the app) and create printable join codes; follow each invitation to joined. Daily cap, de-duplicated per address, and the answer never says whether an address has an account. |
members.read | List the organisation's linked members as opaque member references. |
messages.read | Read your organisation's in-app conversation with a member, including their replies (audited as a disclosure). |
messages.write | Send a member a secure in-app message from your organisation (the conversation your staff use in Norvya); Norvya notifies them with its own generic wording, never your text. Members who turned messages off are refused. |
Endpoints
Invite a person to join your organisation on Norvya
Norvya emails its own invitation (your organisation's name and branding, the person's language) with a link and a one-time code that open the app's join screen. The person signs up or signs in and chooses there what to share with you; nothing is pre-granted. The answer is the same whether or not the address already has a Norvya account. An open invitation to the same address is returned instead of a new one (deduplicated: true). A phone number is recorded but never texted by Norvya: with no email, deliver join_url through your own consented channel. Sandbox: recorded, never emailed (simulated: true); use simulate-join.
Request body
Content type: application/json (required)
Where Norvya sends its invitation email.
E.164 (+15551234567). Norvya does NOT text it (see SMS / WhatsApp below): with no email the invitation is recorded with channel none and you deliver join_url yourself.
Used for the greeting only; never stored.
One of Norvya's 17 app languages: en es fr de it zh ja pt ar hi ko ur id vi th ru fa.
Your own id for the person (returned to your client only).
Sections you will ask the person to share: analyses, photos, medical_history, family_history, medications, labs, biometrics, nutrition, skin_scans, longevity, sexual_health, sud, gender_care, infectious_disease, sogi, mental_health. Recorded only; the person chooses in the app.
All responses
201Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s -X POST "https://norvya.fly.dev/v1/invites" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"phone": "+15551234567",
"first_name": "Alex",
"language": "es",
"external_id": "CRM-00042",
"sharing_request": [
"analyses",
"biometrics"
],
"expires_in_days": 14
}'import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
resp = requests.post(
BASE + "/v1/invites",
headers={"Authorization": f"Bearer {TOKEN}"},
json={
"email": "[email protected]",
"phone": "+15551234567",
"first_name": "Alex",
"language": "es",
"external_id": "CRM-00042",
"sharing_request": [
"analyses",
"biometrics",
],
"expires_in_days": 14,
},
timeout=30,
)
resp.raise_for_status()
print(resp.json())// Node.js 18+ (ES module). Keep production credentials on your server.
const BASE = "https://norvya.fly.dev";
const url = new URL(`${BASE}/v1/invites`);
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"email": "[email protected]",
"phone": "+15551234567",
"first_name": "Alex",
"language": "es",
"external_id": "CRM-00042",
"sharing_request": [
"analyses",
"biometrics"
],
"expires_in_days": 14
}),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());{
"invite_ref": "inv_Yc3l0bVhR2vXqk9d8Lw1rA",
"status": "sent",
"channel": "email",
"language": "string",
"external_id": "string",
"sharing_request": [
"string"
],
"member_ref": "mr_3f9c1a0b7d2e4c6a8b1d0e2f",
"simulated": false,
"created_at": "string",
"sent_at": "string",
"opened_at": "string",
"joined_at": "string",
"expires_at": "string",
"revoked_at": "string",
"deduplicated": false,
"code": "NV-7KQ2M-X9RTA",
"join_url": "norvya://join?code=NV-7KQ2M-X9RTA",
"notice": "string"
}The response is the shape from the schema, not live data.
Try it in your sandbox
Your invitations
Newest first. member_ref appears once the person joined and while they stay linked. No address, name or code is ever returned here.
Parameters
sent | not_sent | send_failed | opened | joined | expired | revoked | declined
All responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s "https://norvya.fly.dev/v1/invites" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
resp = requests.get(
BASE + "/v1/invites",
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=30,
)
resp.raise_for_status()
print(resp.json())// Node.js 18+ (ES module). Keep production credentials on your server.
const BASE = "https://norvya.fly.dev";
const url = new URL(`${BASE}/v1/invites`);
const res = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());{
"total": 0,
"offset": 0,
"limit": 0,
"items": [
{
"invite_ref": "inv_Yc3l0bVhR2vXqk9d8Lw1rA",
"status": "sent",
"channel": "email",
"language": "string",
"external_id": "string",
"sharing_request": [
"string"
],
"member_ref": "mr_3f9c1a0b7d2e4c6a8b1d0e2f",
"simulated": false,
"created_at": "string",
"sent_at": "string",
"opened_at": "string",
"joined_at": "string",
"expires_at": "string",
"revoked_at": "string"
}
]
}The response is the shape from the schema, not live data.
Try it in your sandbox
One invitation's status
Parameters
All responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s "https://norvya.fly.dev/v1/invites/$INVITE_REF" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
invite_ref = os.environ["INVITE_REF"]
resp = requests.get(
f"{BASE}/v1/invites/{invite_ref}",
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=30,
)
resp.raise_for_status()
print(resp.json())// Node.js 18+ (ES module). Keep production credentials on your server.
const BASE = "https://norvya.fly.dev";
const invite_ref = process.env.INVITE_REF;
const url = new URL(`${BASE}/v1/invites/${encodeURIComponent(invite_ref)}`);
const res = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());{
"invite_ref": "inv_Yc3l0bVhR2vXqk9d8Lw1rA",
"status": "sent",
"channel": "email",
"language": "string",
"external_id": "string",
"sharing_request": [
"string"
],
"member_ref": "mr_3f9c1a0b7d2e4c6a8b1d0e2f",
"simulated": false,
"created_at": "string",
"sent_at": "string",
"opened_at": "string",
"joined_at": "string",
"expires_at": "string",
"revoked_at": "string"
}The response is the shape from the schema, not live data.
Try it in your sandbox
Revoke an invitation
Its code stops working at once. Idempotent; a joined invitation cannot be revoked (409 invite_already_joined: the member leaves from the app).
Parameters
Errors specific to this call
409invite_already_joinedAll responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s -X POST "https://norvya.fly.dev/v1/invites/$INVITE_REF/revoke" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
invite_ref = os.environ["INVITE_REF"]
resp = requests.post(
f"{BASE}/v1/invites/{invite_ref}/revoke",
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=30,
)
resp.raise_for_status()
print(resp.json())// Node.js 18+ (ES module). Keep production credentials on your server.
const BASE = "https://norvya.fly.dev";
const invite_ref = process.env.INVITE_REF;
const url = new URL(`${BASE}/v1/invites/${encodeURIComponent(invite_ref)}/revoke`);
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());{
"invite_ref": "inv_Yc3l0bVhR2vXqk9d8Lw1rA",
"status": "sent",
"channel": "email",
"language": "string",
"external_id": "string",
"sharing_request": [
"string"
],
"member_ref": "mr_3f9c1a0b7d2e4c6a8b1d0e2f",
"simulated": false,
"created_at": "string",
"sent_at": "string",
"opened_at": "string",
"joined_at": "string",
"expires_at": "string",
"revoked_at": "string"
}The response is the shape from the schema, not live data.
Try it in your sandbox
Sandbox: a synthetic member joins through this invitation
Sandbox credentials only (403 sandbox_only otherwise). Creates a synthetic member (HTEST labels), links it as the app's join would, marks the invitation joined and queues member.joined.
Parameters
Errors specific to this call
403sandbox_onlyAll responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s -X POST "https://norvya.fly.dev/v1/invites/$INVITE_REF/simulate-join" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
invite_ref = os.environ["INVITE_REF"]
resp = requests.post(
f"{BASE}/v1/invites/{invite_ref}/simulate-join",
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=30,
)
resp.raise_for_status()
print(resp.json())// Node.js 18+ (ES module). Keep production credentials on your server.
const BASE = "https://norvya.fly.dev";
const invite_ref = process.env.INVITE_REF;
const url = new URL(`${BASE}/v1/invites/${encodeURIComponent(invite_ref)}/simulate-join`);
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());{
"member_ref": "string",
"invite_ref": "string",
"code_ref": "string",
"simulated": false
}The response is the shape from the schema, not live data.
Try it in your sandbox
Your join codes
All responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s "https://norvya.fly.dev/v1/join-codes" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
resp = requests.get(
BASE + "/v1/join-codes",
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=30,
)
resp.raise_for_status()
print(resp.json())// Node.js 18+ (ES module). Keep production credentials on your server.
const BASE = "https://norvya.fly.dev";
const url = new URL(`${BASE}/v1/join-codes`);
const res = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());{
"items": [
{
"code_ref": "jc_q1w2e3r4t5y6u7i8o9p0aa",
"code": "JN-7KQ2MX9R",
"join_url": "norvya://join?code=JN-7KQ2MX9R",
"label": "string",
"status": "active",
"max_uses": 0,
"uses": 0,
"expires_at": "string",
"created_at": "string",
"last_used_at": "string",
"revoked_at": "string",
"simulated": false
}
]
}The response is the shape from the schema, not live data.
Try it in your sandbox
Create a printable join code for your organisation
A code (and a join_url) to print or send yourself. A person enters it on the app's join screen exactly like your clinic code and chooses what to share. Optional expiry and use limit; at most 50 live codes.
Request body
Content type: application/json
All responses
201Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s -X POST "https://norvya.fly.dev/v1/join-codes" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"label": "Front desk poster",
"max_uses": 500,
"expires_in_days": 90
}'import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
resp = requests.post(
BASE + "/v1/join-codes",
headers={"Authorization": f"Bearer {TOKEN}"},
json={
"label": "Front desk poster",
"max_uses": 500,
"expires_in_days": 90,
},
timeout=30,
)
resp.raise_for_status()
print(resp.json())// Node.js 18+ (ES module). Keep production credentials on your server.
const BASE = "https://norvya.fly.dev";
const url = new URL(`${BASE}/v1/join-codes`);
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"label": "Front desk poster",
"max_uses": 500,
"expires_in_days": 90
}),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());{
"code_ref": "jc_q1w2e3r4t5y6u7i8o9p0aa",
"code": "JN-7KQ2MX9R",
"join_url": "norvya://join?code=JN-7KQ2MX9R",
"label": "string",
"status": "active",
"max_uses": 0,
"uses": 0,
"expires_at": "string",
"created_at": "string",
"last_used_at": "string",
"revoked_at": "string",
"simulated": false
}The response is the shape from the schema, not live data.
Try it in your sandbox
Revoke a join code
Parameters
All responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s -X POST "https://norvya.fly.dev/v1/join-codes/$CODE_REF/revoke" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
code_ref = os.environ["CODE_REF"]
resp = requests.post(
f"{BASE}/v1/join-codes/{code_ref}/revoke",
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=30,
)
resp.raise_for_status()
print(resp.json())// Node.js 18+ (ES module). Keep production credentials on your server.
const BASE = "https://norvya.fly.dev";
const code_ref = process.env.CODE_REF;
const url = new URL(`${BASE}/v1/join-codes/${encodeURIComponent(code_ref)}/revoke`);
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());{
"code_ref": "jc_q1w2e3r4t5y6u7i8o9p0aa",
"code": "JN-7KQ2MX9R",
"join_url": "norvya://join?code=JN-7KQ2MX9R",
"label": "string",
"status": "active",
"max_uses": 0,
"uses": 0,
"expires_at": "string",
"created_at": "string",
"last_used_at": "string",
"revoked_at": "string",
"simulated": false
}The response is the shape from the schema, not live data.
Try it in your sandbox
Sandbox: a synthetic member joins with this code
Parameters
All responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s -X POST "https://norvya.fly.dev/v1/join-codes/$CODE_REF/simulate-join" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
code_ref = os.environ["CODE_REF"]
resp = requests.post(
f"{BASE}/v1/join-codes/{code_ref}/simulate-join",
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=30,
)
resp.raise_for_status()
print(resp.json())// Node.js 18+ (ES module). Keep production credentials on your server.
const BASE = "https://norvya.fly.dev";
const code_ref = process.env.CODE_REF;
const url = new URL(`${BASE}/v1/join-codes/${encodeURIComponent(code_ref)}/simulate-join`);
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());{
"member_ref": "string",
"invite_ref": "string",
"code_ref": "string",
"simulated": false
}The response is the shape from the schema, not live data.
Try it in your sandbox
Send a member a secure in-app message
The message lands in your organisation's conversation with the member (the one your staff use in Norvya) and the member gets Norvya's generic notification ("Message from your care team"), never your text, through their own settings: push, or a text only if they opted in for you; quiet hours and caps apply. Plain text, at most 2000 characters, no attachments in v1. 409 member_messages_off when the member turned messages from you off; 429 member_message_cap_reached (PARTNER_MESSAGES_PER_MEMBER_PER_DAY, default 5 a day) or message_daily_cap_reached. Sandbox / synthetic members: written, never sent (delivery simulated).
Parameters
EMR vendors also pass vendor (the practice) with a vendor-issued token. EMR vendor tokens
Request body
Content type: application/json (required)
Plain text shown in the app only. Never put in the push, a text or an email.
Reply into the conversation containing this message_ref; omit to start a new one.
Errors specific to this call
409member_messages_off429member_message_cap_reachedAll responses
201Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s -X POST "https://norvya.fly.dev/v1/members/$MEMBER_REF/messages" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"body": "Your class pass renews next week.",
"subject": "Membership",
"thread_ref": "msg_5e7a9c1b3d5f7a9c1e3a5b7d"
}'import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
member_ref = os.environ["MEMBER_REF"]
resp = requests.post(
f"{BASE}/v1/members/{member_ref}/messages",
headers={"Authorization": f"Bearer {TOKEN}"},
json={
"body": "Your class pass renews next week.",
"subject": "Membership",
"thread_ref": "msg_5e7a9c1b3d5f7a9c1e3a5b7d",
},
timeout=30,
)
resp.raise_for_status()
print(resp.json())// Node.js 18+ (ES module). Keep production credentials on your server.
const BASE = "https://norvya.fly.dev";
const member_ref = process.env.MEMBER_REF;
const url = new URL(`${BASE}/v1/members/${encodeURIComponent(member_ref)}/messages`);
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"body": "Your class pass renews next week.",
"subject": "Membership",
"thread_ref": "msg_5e7a9c1b3d5f7a9c1e3a5b7d"
}),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());{
"member_ref": "string",
"message": {
"message_ref": "msg_5e7a9c1b3d5f7a9c1e3a5b7d",
"thread_ref": "string",
"sender": "organisation",
"sent_via": "partner_api",
"message_type": "message",
"subject": "string",
"body": "string",
"attachment_count": 0,
"created_at": "string",
"read_at": "string"
},
"delivery": "notified"
}The response is the shape from the schema, not live data.
Try it in your sandbox
Your organisation's conversation with a member
Oldest first (the newest `limit`), as your staff see it in Norvya: messages from your organisation (sent_via partner_api or norvya) and the member's replies. Staff names are never returned. Reading never marks a message read. Audited as a disclosure.
Parameters
ISO 8601: only messages after this instant.
EMR vendors also pass vendor (the practice) with a vendor-issued token. EMR vendor tokens
All responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s "https://norvya.fly.dev/v1/members/$MEMBER_REF/messages" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
member_ref = os.environ["MEMBER_REF"]
resp = requests.get(
f"{BASE}/v1/members/{member_ref}/messages",
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=30,
)
resp.raise_for_status()
print(resp.json())// Node.js 18+ (ES module). Keep production credentials on your server.
const BASE = "https://norvya.fly.dev";
const member_ref = process.env.MEMBER_REF;
const url = new URL(`${BASE}/v1/members/${encodeURIComponent(member_ref)}/messages`);
const res = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());{
"member_ref": "string",
"messages_allowed": false,
"items": [
{
"message_ref": "msg_5e7a9c1b3d5f7a9c1e3a5b7d",
"thread_ref": "string",
"sender": "organisation",
"sent_via": "partner_api",
"message_type": "message",
"subject": "string",
"body": "string",
"attachment_count": 0,
"created_at": "string",
"read_at": "string"
}
]
}The response is the shape from the schema, not live data.
Try it in your sandbox
Sandbox: the synthetic member replies
Sandbox credentials only (403 sandbox_only otherwise). A fixed reply from the synthetic member through the same hook every member message passes, so message.received fires exactly as it would in production.
Parameters
EMR vendors also pass vendor (the practice) with a vendor-issued token. EMR vendor tokens
Errors specific to this call
403sandbox_onlyAll responses
201Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s -X POST "https://norvya.fly.dev/v1/members/$MEMBER_REF/messages/simulate-reply" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
member_ref = os.environ["MEMBER_REF"]
resp = requests.post(
f"{BASE}/v1/members/{member_ref}/messages/simulate-reply",
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=30,
)
resp.raise_for_status()
print(resp.json())// Node.js 18+ (ES module). Keep production credentials on your server.
const BASE = "https://norvya.fly.dev";
const member_ref = process.env.MEMBER_REF;
const url = new URL(`${BASE}/v1/members/${encodeURIComponent(member_ref)}/messages/simulate-reply`);
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());{
"message_ref": "msg_5e7a9c1b3d5f7a9c1e3a5b7d",
"thread_ref": "string",
"sender": "organisation",
"sent_via": "partner_api",
"message_type": "message",
"subject": "string",
"body": "string",
"attachment_count": 0,
"created_at": "string",
"read_at": "string"
}The response is the shape from the schema, not live data.
Try it in your sandbox
Webhook events
Payloads carry no health data, only a reference to fetch with your token. Each event needs its scope as well as webhooks.manage. Signing and retries
| Event | Scope | When |
|---|---|---|
member.joined | members.read | A person linked to your organisation from the app's join screen. |
message.received | messages.read | A member wrote to your organisation in its in-app conversation. |
Generated from the Partner API router and developers_catalog.py. The router docstring is the source of each description.