Norvya Developers
Get a sandbox
API reference · Foundation

Enrolment & messaging

Built — gated12 endpoints · 2 webhook events · OpenAPI tag engagement

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

Scopes

ScopeAllows
members.inviteInvite 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.readList the organisation's linked members as opaque member references.
messages.readRead your organisation's in-app conversation with a member, including their replies (audited as a disclosure).
messages.writeSend 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

POST/v1/invitesmembers.invite

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)

emailstring | null

Where Norvya sends its invitation email.

phonestring | null

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.

first_namestring | null

Used for the greeting only; never stored.

languagestring | null

One of Norvya's 17 app languages: en es fr de it zh ja pt ar hi ko ur id vi th ru fa.

external_idstring | null

Your own id for the person (returned to your client only).

sharing_requestarray of string | null

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.

expires_in_daysinteger | null

All responses

201Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service Unavailable

Codes 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
  }'
Response 201
{
  "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.

Your invitations

GET/v1/invitesmembers.invite

Newest first. member_ref appears once the person joined and while they stay linked. No address, name or code is ever returned here.

Parameters

statusquery · string | null

sent | not_sent | send_failed | opened | joined | expired | revoked | declined

offsetquery · integer
limitquery · integer

All responses

200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service Unavailable

Codes every call can return are in Errors & limits.

curl -s "https://norvya.fly.dev/v1/invites" \
  -H "Authorization: Bearer $TOKEN"
Response 200
{
  "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.

One invitation's status

GET/v1/invites/{invite_ref}members.invite

Parameters

invite_refpath · stringrequired

All responses

200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service Unavailable

Codes every call can return are in Errors & limits.

curl -s "https://norvya.fly.dev/v1/invites/$INVITE_REF" \
  -H "Authorization: Bearer $TOKEN"
Response 200
{
  "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.

Revoke an invitation

POST/v1/invites/{invite_ref}/revokemembers.invite

Its code stops working at once. Idempotent; a joined invitation cannot be revoked (409 invite_already_joined: the member leaves from the app).

Parameters

invite_refpath · stringrequired

Errors specific to this call

409invite_already_joined

All responses

200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service Unavailable

Codes 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"
Response 200
{
  "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.

Sandbox: a synthetic member joins through this invitation

POST/v1/invites/{invite_ref}/simulate-joinmembers.invite

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

invite_refpath · stringrequired

Errors specific to this call

403sandbox_only

All responses

200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service Unavailable

Codes 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"
Response 200
{
  "member_ref": "string",
  "invite_ref": "string",
  "code_ref": "string",
  "simulated": false
}

The response is the shape from the schema, not live data.

Your join codes

GET/v1/join-codesmembers.invite

All responses

200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service Unavailable

Codes every call can return are in Errors & limits.

curl -s "https://norvya.fly.dev/v1/join-codes" \
  -H "Authorization: Bearer $TOKEN"
Response 200
{
  "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.

Create a printable join code for your organisation

POST/v1/join-codesmembers.invite

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 Unavailable

Codes 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
  }'
Response 201
{
  "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.

Revoke a join code

POST/v1/join-codes/{code_ref}/revokemembers.invite

Parameters

code_refpath · stringrequired

All responses

200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service Unavailable

Codes 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"
Response 200
{
  "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.

Sandbox: a synthetic member joins with this code

POST/v1/join-codes/{code_ref}/simulate-joinmembers.invite

Parameters

code_refpath · stringrequired

All responses

200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service Unavailable

Codes 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"
Response 200
{
  "member_ref": "string",
  "invite_ref": "string",
  "code_ref": "string",
  "simulated": false
}

The response is the shape from the schema, not live data.

Send a member a secure in-app message

POST/v1/members/{member_ref}/messagesmessages.write

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

member_refpath · stringrequired

EMR vendors also pass vendor (the practice) with a vendor-issued token. EMR vendor tokens

Request body

Content type: application/json (required)

bodystringrequired

Plain text shown in the app only. Never put in the push, a text or an email.

subjectstring | null
thread_refstring | null

Reply into the conversation containing this message_ref; omit to start a new one.

Errors specific to this call

409member_messages_off429member_message_cap_reached

All responses

201Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service Unavailable

Codes 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"
  }'
Response 201
{
  "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.

Your organisation's conversation with a member

GET/v1/members/{member_ref}/messagesmessages.read

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

member_refpath · stringrequired
sincequery · string | null

ISO 8601: only messages after this instant.

limitquery · integer

EMR vendors also pass vendor (the practice) with a vendor-issued token. EMR vendor tokens

All responses

200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service Unavailable

Codes every call can return are in Errors & limits.

curl -s "https://norvya.fly.dev/v1/members/$MEMBER_REF/messages" \
  -H "Authorization: Bearer $TOKEN"
Response 200
{
  "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.

Sandbox: the synthetic member replies

POST/v1/members/{member_ref}/messages/simulate-replymessages.write

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

member_refpath · stringrequired

EMR vendors also pass vendor (the practice) with a vendor-issued token. EMR vendor tokens

Errors specific to this call

403sandbox_only

All responses

201Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service Unavailable

Codes 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"
Response 201
{
  "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.

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

EventScopeWhen
member.joinedmembers.readA person linked to your organisation from the app's join screen.
message.receivedmessages.readA 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.

↑ ↓ to moveEnter to open