Manage email recipients

An email recipient is a person named on your audits, such as a provider, with the address their audit result emails go to. Use /v2/email_recipients to create email recipients, update them, find them by email and delete them, so they match your own roster. None of these calls sends an email or changes which audits name the person.

Prerequisites

  • An access token in $BRELLIUM_ACCESS_TOKEN, cached and reused (Authenticate with OAuth2 client credentials).
  • An email recipient's person_id for steps 3 to 5. This page uses 2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d, the provider on the sample audit.
  • With a live key, email recipients are protected health information (PHI), so test with fictional people or a sandbox key.
  • In the sandbox, the sample recipient already has the address step 1 sends and is linked to the sample user. With a sandbox key, step 1 returns 409 EMAIL_RECIPIENT_EXISTS and step 5 returns 409 EMAIL_RECIPIENT_HAS_USER, so use another address in step 1 and delete the recipient it creates.

Steps

Create an email recipient

POST /v2/email_recipients creates an email recipient. All four fields are required: first_name, last_name, email and metadata (a JSON object, {} when not set). Send Content-Type: application/json with every request body; any other content type, or none, returns 400 INVALID_CONTENT_TYPE.

curl "https://api.brellium.com/v2/email_recipients" \
  --request POST \
  --header "Authorization: Bearer $BRELLIUM_ACCESS_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "first_name": "Jordan",
    "last_name": "Rivera",
    "email": "Jordan.Rivera@clinic.example",
    "metadata": { "roster_team": "North clinic" }
  }' \
  --write-out "HTTP %{http_code}\n"
const res = await fetch("https://api.brellium.com/v2/email_recipients", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BRELLIUM_ACCESS_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    first_name: "Jordan",
    last_name: "Rivera",
    email: "Jordan.Rivera@clinic.example",
    metadata: { roster_team: "North clinic" },
  }),
});
if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
console.log(await res.json(), `HTTP ${res.status}`);
import BrelliumClient from "@brellium/api-client";

const client = new BrelliumClient({ oAuth2: process.env.BRELLIUM_ACCESS_TOKEN });

const recipient = await client.emailRecipients.create({
  first_name: "Jordan",
  last_name: "Rivera",
  email: "Jordan.Rivera@clinic.example",
  metadata: { roster_team: "North clinic" },
});
console.log(recipient);
import os

from brellium_api_client import BrelliumClient

client = BrelliumClient(o_auth2=os.environ["BRELLIUM_ACCESS_TOKEN"])

recipient = client.email_recipients.create(
    first_name="Jordan",
    last_name="Rivera",
    email="Jordan.Rivera@clinic.example",
    metadata={"roster_team": "North clinic"},
)
print(recipient.to_json())

The full programs below send the same request. They return the recipient on a 201 and throw on any other status, with the error code and request_id.

// Keep credentials on the server; never in a browser or mobile app.
const token = process.env.BRELLIUM_ACCESS_TOKEN;
if (!token) throw new Error("BRELLIUM_ACCESS_TOKEN is not set");

const BASE_URL = "https://api.brellium.com";

type EmailRecipient = {
  person_id: string;
  object: "email_recipient";
  first_name: string;
  last_name: string;
  email: string | null;
  metadata: Record<string, unknown>;
  created_at: string;
  updated_at: string;
};
type ApiError = {
  error: {
    type: string;
    code: string;
    message: string;
    param?: string;
    details?: unknown;
    request_id?: string;
  };
};
type RecipientFields = {
  first_name: string;
  last_name: string;
  email: string;
  metadata: Record<string, unknown>;
};

// 201: created, or a deleted recipient restored under its old person_id.
// 409 EMAIL_RECIPIENT_EXISTS: another recipient already has this address.
async function createRecipient(body: RecipientFields): Promise<EmailRecipient> {
  const res = await fetch(`${BASE_URL}/v2/email_recipients`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });
  if (res.status === 201) return (await res.json()) as EmailRecipient;
  const { error } = (await res.json()) as ApiError;
  throw new Error(
    `${res.status} ${error.code}${error.param ? ` on ${error.param}` : ""}: ` +
      `${error.message} (request_id ${error.request_id})`,
  );
}

async function main() {
  const recipient = await createRecipient({
    first_name: "Jordan",
    last_name: "Rivera",
    email: "Jordan.Rivera@clinic.example",
    metadata: { roster_team: "North clinic" },
  });
  // Store recipient.person_id: audits name this person with the same value in
  // people[].person_id.
  console.log(recipient.person_id, recipient.email);
}

main();
# Keep credentials on the server; never in a browser or mobile app.
import os

import requests

TOKEN = os.environ["BRELLIUM_ACCESS_TOKEN"]
BASE_URL = "https://api.brellium.com"
HEADERS = {
    "Authorization": f"Bearer {TOKEN}",
    "Content-Type": "application/json",
}


def create_recipient(
    first_name: str, last_name: str, email: str, metadata: dict
) -> dict:
    """201: created, or a deleted recipient restored under its old person_id.
    409 EMAIL_RECIPIENT_EXISTS: another recipient already has this address."""
    body = {
        "first_name": first_name,
        "last_name": last_name,
        "email": email,
        "metadata": metadata,
    }
    res = requests.post(
        f"{BASE_URL}/v2/email_recipients", headers=HEADERS, json=body, timeout=30
    )
    if res.status_code == 201:
        return res.json()
    err = res.json()["error"]
    where = f" on {err['param']}" if err.get("param") else ""
    raise RuntimeError(
        f"{res.status_code} {err['code']}{where}: {err['message']} "
        f"(request_id {err.get('request_id')})"
    )


recipient = create_recipient(
    "Jordan",
    "Rivera",
    "Jordan.Rivera@clinic.example",
    {"roster_team": "North clinic"},
)
# Store recipient["person_id"]: audits name this person with the same value in
# people[].person_id.
print(recipient["person_id"], recipient["email"])

The response is 201 with the recipient:

{
  // store this; audits show it in people[].person_id
  "person_id": "2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d",
  "object": "email_recipient",
  "first_name": "Jordan",
  "last_name": "Rivera",
  // stored in lowercase, matched ignoring case
  "email": "jordan.rivera@clinic.example",
  // replaced whole on every update
  "metadata": { "roster_team": "North clinic" },
  "created_at": "2026-09-01T12:00:00.000Z",
  "updated_at": "2026-09-01T12:00:00.000Z"
}

GET /v2/email_recipients/{person_id} and lists can take a few seconds to include a new or changed recipient. A create or update response is the current record, so use it instead of fetching the record again.

Handle the create conflict

The API allows one email recipient per address in your organization, ignoring case. A second POST with the same email (the step 1 call again) returns 409 EMAIL_RECIPIENT_EXISTS:

{
  "error": {
    "type": "conflict_error",
    "code": "EMAIL_RECIPIENT_EXISTS",
    "message": "An email recipient with this email already exists.",
    "param": "email",
    "request_id": "9c1f0a7e-4b2d-4e3a-8f6b-2d1c0b9a8e7f"
  }
}

A person who is not an email recipient, such as a patient, doesn't cause this conflict. To resolve it, find the existing recipient with ?email= (step 4) and PATCH it.

A POST with a deleted recipient's address restores that recipient with 201. It keeps its person_id and created_at, so audits still point at it, and takes every other field from your request. A restored recipient that is linked to a dashboard user (a user who signs in to Brellium) stays linked. When several deleted recipients have the address, the API restores the most recently changed one.

Update an email recipient

PATCH /v2/email_recipients/{person_id} requires all four fields and replaces them, so send the current value of any field you want to keep. The PATCH replaces the whole metadata object, so fetch the recipient first, change what you need and send all four fields back.

curl \
  "https://api.brellium.com/v2/email_recipients/2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d" \
  --request PATCH \
  --header "Authorization: Bearer $BRELLIUM_ACCESS_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "first_name": "Jordan",
    "last_name": "Rivera-Ortiz",
    "email": "jordan.rivera@clinic.example",
    "metadata": { "roster_team": "North clinic", "site": "north" }
  }' \
  --write-out "HTTP %{http_code}\n"
const res = await fetch(
  "https://api.brellium.com/v2/email_recipients/2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d",
  {
    method: "PATCH",
    headers: {
      Authorization: `Bearer ${process.env.BRELLIUM_ACCESS_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      first_name: "Jordan",
      last_name: "Rivera-Ortiz",
      email: "jordan.rivera@clinic.example",
      metadata: { roster_team: "North clinic", site: "north" },
    }),
  },
);
if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
console.log(await res.json(), `HTTP ${res.status}`);
import BrelliumClient from "@brellium/api-client";

const client = new BrelliumClient({ oAuth2: process.env.BRELLIUM_ACCESS_TOKEN });

const recipient = await client.emailRecipients.update(
  "2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d",
  {
    first_name: "Jordan",
    last_name: "Rivera-Ortiz",
    email: "jordan.rivera@clinic.example",
    metadata: { roster_team: "North clinic", site: "north" },
  },
);
console.log(recipient);
import os

from brellium_api_client import BrelliumClient

client = BrelliumClient(o_auth2=os.environ["BRELLIUM_ACCESS_TOKEN"])

recipient = client.email_recipients.update(
    "2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d",
    first_name="Jordan",
    last_name="Rivera-Ortiz",
    email="jordan.rivera@clinic.example",
    metadata={"roster_team": "North clinic", "site": "north"},
)
print(recipient.to_json())

The full programs below fetch the recipient, change last_name, add site to metadata and send all four fields back. On a 404 they print that your organization has no email recipient with that person_id. On 409 EMAIL_RECIPIENT_HAS_USER they stop with an error that names the PATCH /v2/users/{user_id} call to make instead.

// Keep credentials on the server; never in a browser or mobile app.
const token = process.env.BRELLIUM_ACCESS_TOKEN;
if (!token) throw new Error("BRELLIUM_ACCESS_TOKEN is not set");

const BASE_URL = "https://api.brellium.com";

type EmailRecipient = {
  person_id: string;
  object: "email_recipient";
  first_name: string;
  last_name: string;
  email: string | null;
  metadata: Record<string, unknown>;
  created_at: string;
  updated_at: string;
};
type ApiError = {
  error: {
    type: string;
    code: string;
    message: string;
    param?: string;
    details?: unknown;
    request_id?: string;
  };
};
type RecipientFields = {
  first_name: string;
  last_name: string;
  email: string;
  metadata: Record<string, unknown>;
};

// Returns null for a 404: an unknown or deleted recipient.
async function updateRecipient(
  personId: string,
  fields: RecipientFields,
): Promise<EmailRecipient | null> {
  const res = await fetch(`${BASE_URL}/v2/email_recipients/${personId}`, {
    method: "PATCH",
    headers: {
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(fields),
  });
  if (res.status === 404) return null;
  if (res.ok) return (await res.json()) as EmailRecipient;
  const { error } = (await res.json()) as ApiError;
  if (error.code === "EMAIL_RECIPIENT_HAS_USER") {
    // The address belongs to a dashboard user: change it with PATCH
    // /v2/users/{user_id}.
    const userId = (error.details as { user_id?: string } | undefined)?.user_id;
    throw new Error(`Change this address with PATCH /v2/users/${userId}`);
  }
  // EMAIL_RECIPIENT_EXISTS, VALIDATION_ERROR, ...
  throw new Error(
    `${res.status} ${error.code}${error.param ? ` on ${error.param}` : ""}: ` +
      `${error.message} (request_id ${error.request_id})`,
  );
}

async function main() {
  const personId = "2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d";
  // Start from the saved recipient so the fields you don't change keep their
  // values.
  const res = await fetch(`${BASE_URL}/v2/email_recipients/${personId}`, {
    headers: { Authorization: `Bearer ${token}` },
  });
  if (res.status === 404) {
    console.log("No email recipient with that person_id in this organization");
    return;
  }
  if (!res.ok) throw new Error(`GET /v2/email_recipients returned ${res.status}`);
  const current = (await res.json()) as EmailRecipient;
  const updated = await updateRecipient(personId, {
    first_name: current.first_name,
    last_name: "Rivera-Ortiz",
    // email is required on PATCH; supply one when the recipient has none
    email: current.email ?? "jordan.rivera@clinic.example",
    metadata: { ...current.metadata, site: "north" },
  });
  if (!updated) {
    console.log("No email recipient with that person_id in this organization");
    return;
  }
  console.log(updated.last_name, updated.updated_at);
}

main();
# Keep credentials on the server; never in a browser or mobile app.
import os

import requests

TOKEN = os.environ["BRELLIUM_ACCESS_TOKEN"]
BASE_URL = "https://api.brellium.com"
HEADERS = {
    "Authorization": f"Bearer {TOKEN}",
    "Content-Type": "application/json",
}


def update_recipient(person_id: str, fields: dict) -> dict | None:
    """Returns None for a 404: an unknown or deleted recipient.
    fields must hold all four of first_name, last_name, email and metadata."""
    res = requests.patch(
        f"{BASE_URL}/v2/email_recipients/{person_id}",
        headers=HEADERS,
        json=fields,
        timeout=30,
    )
    if res.status_code == 404:
        return None
    if res.ok:
        return res.json()
    err = res.json()["error"]
    if err["code"] == "EMAIL_RECIPIENT_HAS_USER":
        # The address belongs to a dashboard user: change it with PATCH
        # /v2/users/{user_id}.
        user_id = (err.get("details") or {}).get("user_id")
        raise RuntimeError(f"Change this address with PATCH /v2/users/{user_id}")
    # EMAIL_RECIPIENT_EXISTS, VALIDATION_ERROR, ...
    where = f" on {err['param']}" if err.get("param") else ""
    raise RuntimeError(
        f"{res.status_code} {err['code']}{where}: {err['message']} "
        f"(request_id {err.get('request_id')})"
    )


person_id = "2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d"
# Start from the saved recipient so the fields you don't change keep their values.
res = requests.get(
    f"{BASE_URL}/v2/email_recipients/{person_id}", headers=HEADERS, timeout=30
)
if res.status_code == 404:
    raise SystemExit("No email recipient with that person_id in this organization")
res.raise_for_status()
current = res.json()
updated = update_recipient(
    person_id,
    {
        "first_name": current["first_name"],
        "last_name": "Rivera-Ortiz",
        # email is required on PATCH; supply one when the recipient has none
        "email": current["email"] or "jordan.rivera@clinic.example",
        "metadata": {**current["metadata"], "site": "north"},
    },
)
if updated is None:
    print("No email recipient with that person_id in this organization")
else:
    print(updated["last_name"], updated["updated_at"])
{
  "person_id": "2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d",
  "last_name": "Rivera-Ortiz",
  // exactly the object you sent
  "metadata": { "roster_team": "North clinic", "site": "north" },
  // changes on every update
  "updated_at": "2026-10-21T15:00:00.000Z"
  // the other fields as in step 1
  /* ... */
}

The response is 200 with the updated recipient. For a few seconds after a change, GET /v2/email_recipients/{person_id} and lists can still return the previous version. A PATCH can also return these errors:

  • 400 VALIDATION_ERROR ("Request validation failed"): a field is missing or invalid, or the person_id in the path is not a UUID; details[].path names it, for example ["metadata"].
  • 404 NOT_FOUND ("Email recipient not found"): your organization has no email recipient with this person_id; another organization's id and a deleted recipient return 404 too.
  • 409 EMAIL_RECIPIENT_EXISTS with param: "email": another email recipient has the new address, ignoring case; the current address is never a conflict.
  • 409 EMAIL_RECIPIENT_HAS_USER: the recipient is also a dashboard user and you sent a different address. Change the address with PATCH /v2/users/{user_id} (Update a user) for the user in details.user_id, and the recipient's address changes with it (Invite and manage users). The API runs this check before the duplicate check, so you get this code even when another recipient has the new address. To change the other fields, resend the current address; the same address in another case is not a change.
{
  "error": {
    "type": "conflict_error",
    "code": "EMAIL_RECIPIENT_HAS_USER",
    "message": "This email recipient is also a dashboard user; change its email with PATCH /v2/users/7c6d5e4f-3a2b-4c1d-9e8f-7a6b5c4d3e2f instead.",
    "param": "email",
    "details": { "user_id": "7c6d5e4f-3a2b-4c1d-9e8f-7a6b5c4d3e2f" },
    "request_id": "38174c7d-a1fc-489b-a3fd-4f010c7ba1e7"
  }
}
Find email recipients

GET /v2/email_recipients returns your organization's email recipients, newest first. email matches one whole address, ignoring case, and a repeated person_id returns specific recipients. created_after and created_before are inclusive and take RFC 3339 timestamps in UTC ending in Z; an offset such as +02:00 or a date alone returns 400 VALIDATION_ERROR.

The filters combine with AND, and include_total=true adds total_count, the number of matches across all pages. A raw + in a query value decodes to a space and matches nothing, so send it as %2B; --data-urlencode does this for you.

# By address: normally one item, none when nobody has it
curl --get "https://api.brellium.com/v2/email_recipients" \
  --header "Authorization: Bearer $BRELLIUM_ACCESS_TOKEN" \
  --data-urlencode "email=Jordan.Rivera@clinic.example"
// By address: normally one item, none when nobody has it
const params = new URLSearchParams({ email: "Jordan.Rivera@clinic.example" });
const res = await fetch(`https://api.brellium.com/v2/email_recipients?${params}`, {
  headers: { Authorization: `Bearer ${process.env.BRELLIUM_ACCESS_TOKEN}` },
});
if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
console.log(await res.json());
import BrelliumClient from "@brellium/api-client";

const client = new BrelliumClient({ oAuth2: process.env.BRELLIUM_ACCESS_TOKEN });

// By address: normally one item, none when nobody has it
const recipients = await client.emailRecipients.list({
  email: "Jordan.Rivera@clinic.example",
});
console.log(recipients);
import os

from brellium_api_client import BrelliumClient

client = BrelliumClient(o_auth2=os.environ["BRELLIUM_ACCESS_TOKEN"])

# By address: normally one item, none when nobody has it
recipients = client.email_recipients.list(email="Jordan.Rivera@clinic.example")
print(recipients.to_json())
{
  "object": "list",
  "data": [
    {
      "person_id": "2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d",
      "email": "jordan.rivera@clinic.example" /* ... */
    } // the recipient from step 3
  ],
  "has_more": false
}

Older recipients can share an address, so allow for more than one item in data.

With repeated person_id values, the list has one item per id that belongs to an email recipient. An id that matches no email recipient in your organization, such as a patient's people[].person_id, is left out, with no error:

# The second person_id is another recipient from your own list.
curl --get "https://api.brellium.com/v2/email_recipients" \
  --data-urlencode "person_id=2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d" \
  --data-urlencode "person_id=6b5a4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d" \
  --header "Authorization: Bearer $BRELLIUM_ACCESS_TOKEN"
const params = new URLSearchParams();
params.append("person_id", "2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d");
// a second recipient from your own list
params.append("person_id", "6b5a4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d");
const res = await fetch(`https://api.brellium.com/v2/email_recipients?${params}`, {
  headers: { Authorization: `Bearer ${process.env.BRELLIUM_ACCESS_TOKEN}` },
});
if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
console.log(await res.json());
import os

from brellium_api_client import BrelliumClient

client = BrelliumClient(o_auth2=os.environ["BRELLIUM_ACCESS_TOKEN"])

recipients = client.email_recipients.list(
    person_id=[
        "2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d",
        # a second recipient from your own list
        "6b5a4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
    ],
)
print(recipients.to_json())
{
  "object": "list",
  "data": [
    {
      "person_id": "6b5a4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
      "email": "sam.okafor@clinic.example" /* ... */
    }, // newest first
    {
      "person_id": "2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d",
      "email": "jordan.rivera@clinic.example" /* ... */
    }
  ],
  "has_more": false
}

When nothing matches, the response is 200 with "data": []. Deleted recipients never appear in a list. limit sets the page size, from 1 to 100, and defaults to 20.

A query string over 2,048 bytes is rejected with a bare 403 and no JSON body, so about 43 person_id values fit in one request; split larger batches. For the pagination rules, see Page through and filter lists.

Delete an email recipient

DELETE /v2/email_recipients/{person_id} deletes an email recipient. Audits that name the person keep their person_id.

curl \
  "https://api.brellium.com/v2/email_recipients/2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d" \
  --request DELETE \
  --header "Authorization: Bearer $BRELLIUM_ACCESS_TOKEN" \
  --write-out "HTTP %{http_code}\n"
const res = await fetch(
  "https://api.brellium.com/v2/email_recipients/2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d",
  {
    method: "DELETE",
    headers: { Authorization: `Bearer ${process.env.BRELLIUM_ACCESS_TOKEN}` },
  },
);
if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
console.log(`HTTP ${res.status}`); // 204, no body
import BrelliumClient from "@brellium/api-client";

const client = new BrelliumClient({ oAuth2: process.env.BRELLIUM_ACCESS_TOKEN });

// 204, no body
await client.emailRecipients.delete("2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d");
console.log("deleted");
import os

from brellium_api_client import BrelliumClient

client = BrelliumClient(o_auth2=os.environ["BRELLIUM_ACCESS_TOKEN"])

# 204, no body
client.email_recipients.delete("2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d")
print("deleted")

The response is 204 with no body. For a few seconds after the delete, GET /v2/email_recipients/{person_id} can still return the recipient and lists can still include it; then GET returns 404. A second DELETE, a PATCH on the deleted recipient, an unknown id, another organization's id and a person who is not an email recipient all return 404 NOT_FOUND ("Email recipient not found").

A deleted person_id still works as a starting_after or ending_before cursor, so a sync that deletes while it pages keeps its place. To bring the person back, POST the same email (step 2).

Deleting a recipient who is also a dashboard user returns 409 EMAIL_RECIPIENT_HAS_USER with details.user_id. Delete the user first with DELETE /v2/users/{user_id} (Delete a user), then delete the recipient.

Expected response

The create, retrieve and update responses and every list item have the eight fields of the recipient in step 1. first_name and last_name take 1 to 255 characters. For the field rules and each response, see Create an email recipient, Retrieve an email recipient, Update an email recipient and Delete an email recipient.

email is null for a recipient with no address on file. metadata is {} until you set it; keep clinical detail out of it. A dashboard user linked to the person has the same person_id.

Common mistakes

Mistake What you see Fix
Creating on every sync run 409 EMAIL_RECIPIENT_EXISTS after the first run Find the recipient with ?email= and PATCH it with all four fields. There is no batch endpoint, so a sync is one call per person.
Retrying the create with a different address Two recipients for one person Reuse the existing person_id and change the address with PATCH.
Adding a brellium.com address 400 VALIDATION_ERROR with param: "email" Addresses at brellium.com and its subdomains can't be email recipients.

Next