---
title: Creating and sharing an API key as a clinic admin
description: Mint an admin bearer API key in Practice Settings, choose its scopes, capture the one-time secret, then share it with an integration partner and rotate it on a schedule.
nav: Recipes
order: 60
version: v1
source: handwritten
updated: 2026-06-26
---

# Creating and sharing an API key as a clinic admin

Mint an admin bearer API key from Practice Settings, pick the scopes your integration
partner actually needs, capture the secret on the one screen that ever shows it, hand it
off without leaking it, and set the rotation habit that keeps the whole thing safe. This
is the workflow you run before a vendor writes a single line of code.

## Audience

You run operations or administration at a clinic — at Clínica San Rafael, that's the
admin who manages access for Doctora María Fernández's integration vendor. You are
comfortable in Practice Settings and on a terminal for one verification command, but you
are not the one building the integration. You decide which data leaves the clinic and who
holds the key.

## You'll need

- An admin-role user on your organization in HuliPractice. Only admins see the API Keys
  surface; a clinician or front-desk login does not.
- A short list from your integration partner of exactly which resources they read and
  write. You translate that list into scopes in step 2 — granting more than they need is
  the most common avoidable risk.
- A secure channel to hand the secret to the partner — a password manager share, an
  enterprise secrets vault, or an equivalent. Plain email and chat do not count.
- `curl` (or any HTTP client) for the single verification request at the end. You can
  also hand the verification step to the partner.

<Callout variant="info">
An admin bearer token is a long-lived credential scoped to one organization — Clínica San
Rafael's data, and nothing from any other clinic. It does not expire on a timer the way a
SMART Backend Services access token does. Treat it like a master key to that organization:
it stays in a secrets manager, never in email, chat, a shared doc, or a code repository.
</Callout>

## End state

A named, active API key exists on your organization with the scopes your partner needs.
You have captured its secret once, shared it through a secure channel, and confirmed it
works with one `200 OK` from a Patient search. You know how to rotate it and how to revoke
it the moment it is no longer needed.

## Steps

### 1. Open the API Keys surface

In HuliPractice, go to **Practice Settings**, open the **Integrations** section, and
select **API Keys**. This page lists every key on your organization with its name, its
scopes, its status, and when it was last used. If the section is absent, you are not
signed in as an admin — switch to an admin login before continuing.

Select **Create API key** to start a new one.

### 2. Name the key and choose its scopes

Give the key a name that identifies the partner and the purpose, not the person who
created it. A name like `San Rafael — Lab results sync` survives staff turnover; `Maria's
key` does not. You will read this name later when you decide what to rotate or revoke, so
make it self-explanatory.

Then select the scopes. Each scope is `system/<Resource>.<perm>`, where the permission
letters are `r` (read), `s` (search), `c` (create), and `u` (update). Two combinations
cover almost every integration:

- Read plus search — the `.rs` form, e.g. <Scope name="system/Patient.rs" />. Grant this
  when the partner only pulls data out.
- Full write — the `.cru` form, e.g. <Scope name="system/Patient.cru" />. Grant this when
  the partner also creates and updates records. It includes read.

Which resources you can grant depends on whether they are writable in v1:

| Resource          | Available scopes                                                                                         |
| ----------------- | -------------------------------------------------------------------------------------------------------- |
| Patient           | <Scope name="system/Patient.rs" /> · <Scope name="system/Patient.cru" />                                 |
| Appointment       | <Scope name="system/Appointment.rs" /> · <Scope name="system/Appointment.cru" />                         |
| Encounter         | <Scope name="system/Encounter.rs" /> · <Scope name="system/Encounter.cru" />                             |
| Observation       | <Scope name="system/Observation.rs" /> · <Scope name="system/Observation.cru" />                         |
| Composition       | <Scope name="system/Composition.rs" /> · <Scope name="system/Composition.cru" /> (BAA-gated)             |
| DocumentReference | <Scope name="system/DocumentReference.rs" /> · <Scope name="system/DocumentReference.cru" /> (BAA-gated) |
| Practitioner      | <Scope name="system/Practitioner.rs" /> (read-only)                                                      |
| Organization      | <Scope name="system/Organization.rs" /> (read-only)                                                      |
| Provenance        | <Scope name="system/Provenance.rs" /> · <Scope name="system/Provenance.c" />                             |

Practitioner and Organization are read-only in v1, so only the `.rs` form exists.
Provenance is the audit-trail resource a partner POSTs alongside a write; it offers
read, search, and create (`.rs` and `.c`), and no update. (Encounters, observations,
medication/service requests, compositions, and document references all sit under one
BAA-gated "Clinical information" card; the webhook Subscription scopes are their own
"Outbound webhooks" card — see the [Scopes reference](/v1/scopes).)

<Callout variant="warning">
Grant the narrowest set that does the job. A lab-results integration that only reads
patients and writes observations needs <Scope name="system/Patient.rs" /> and
<Scope name="system/Observation.cru" /> — not write access to appointments or encounters.
Every extra scope widens what a leaked key exposes. You can mint a second, separate key
for a second partner rather than over-scoping one shared key.
</Callout>

<Callout variant="warning">
**Clinical scopes require a signed BAA.** The sensitive **Clinical information** card
(Encounter, Observation, MedicationRequest, ServiceRequest, Composition, DocumentReference)
exposes protected health information, so the surface makes
you attest to a Business Associate Agreement (or equivalent) before it will mint a key that
carries any of those scopes. Demographic and scheduling scopes (Patient, Appointment,
Practitioner, Organization) are not gated. Only enable a clinical card for a partner you have a
BAA with.
</Callout>

### 3. Capture the secret — it is shown once

On confirmation, the surface displays the full secret token one time. This is the only
moment the secret is ever visible — Practice Settings stores a hash, not the token, so it
cannot show the value again on any later visit.

Copy the secret immediately and place it in your secrets manager before you leave or close
the screen. If you navigate away without copying it, the key still exists but its secret is
unrecoverable; your only path is to delete that key and mint a new one (steps 1–3).

After you have stored the secret, close the reveal. The list now shows the key as active,
with its name and scopes, but never the secret again.

### 4. Share it with your integration partner

Hand the secret to the partner through the secure channel you prepared — a password
manager share or a secrets vault, scoped to just the people who operate the integration.
Send the partner three things:

- The secret token itself (through the secure channel, never inline in a message).
- The base URL, <InlineCode>https://api.huli.ai</InlineCode>, with the FHIR R4 path under
  <InlineCode>/fhir/R4/</InlineCode>.
- The list of scopes you granted, so the partner builds against exactly what the key
  allows and is not surprised by a <StatusBadge code="403" /> on a resource you withheld.

The partner authenticates by sending the token as an HTTP `Authorization: Bearer` header
on every request. They do not call the token endpoint and they do not need your private
keys — the admin bearer token is the credential as-is.

<Callout variant="danger">
Do not paste the secret into email, chat, a ticket, a shared spreadsheet, or a code
repository, and do not screenshot the reveal screen into any of those. A token in a chat
log is a token in everyone's search history. If it lands in one of those places even once,
treat it as compromised and rotate it (step 6).
</Callout>

### 5. Verify the key works

Run one authenticated request to confirm the key is live and correctly scoped. This
example searches patients by name, so it needs a key with <Scope name="system/Patient.rs" />
(or <Scope name="system/Patient.cru" />). You can run it yourself or hand it to the
partner.

```bash
export HULI_API_KEY="<paste the secret you stored in step 3>"

curl "https://api.huli.ai/fhir/R4/Patient?name=Fern%C3%A1ndez&_count=20" \
  -H "Authorization: Bearer $HULI_API_KEY" \
  -H "Accept: application/fhir+json"
```

A working key returns <StatusBadge code="200" /> with a FHIR `Bundle` of type
`searchset`. Encode the accent in the name (`á` becomes `%C3%A1`) so the search matches —
this query looks up Doctora María Fernández's patients whose name starts with `Fernández`.

### 6. Rotate and revoke

Rotation replaces a key's secret without an outage: mint a new key (steps 1–3), share it
with the partner (step 4), let them cut over and verify (step 5), then revoke the old one.
Two keys are valid at once during the overlap, so nothing breaks mid-cutover. Rotate on a
schedule — a calendar reminder per partner — and immediately if a secret is ever exposed.

To revoke, return to **Practice Settings → Integrations → API Keys**, find the key by the
name you gave it in step 2, and revoke it. A revoked key stops authenticating right away;
every request with it then returns <StatusBadge code="401" /> with `HPB-00106` — see What
can go wrong. Revoke a key the moment a partnership ends or a key is no longer in use — an
unused active key is pure risk with no upside.

<Callout variant="note">
The name you chose in step 2 is what makes rotation and revocation safe. When you hold
three keys for three partners, `San Rafael — Lab results sync` tells you exactly which one
to revoke; `key 3` does not. This is why the naming convention is worth the few extra
seconds up front.
</Callout>

## What to verify

- The new key appears in **Practice Settings → Integrations → API Keys** as active, with
  the name and scopes you intended.
- You captured the secret on the one-time reveal and stored it in a secrets manager — not
  in email, chat, or a repository.
- The verification request in step 5 returns <StatusBadge code="200" /> with a `Bundle` of
  type `searchset`.
- The granted scopes match the partner's stated need and no more.
- You have a rotation reminder set and you know which list row to revoke when the
  partnership ends.

## What can go wrong

The API returns errors as a FHIR `OperationOutcome`, not a bare string. The HTTP status and
`issue[0].code` (the FHIR IssueType) classify the failure; the Huli code (`HPB-…`) is the
prefix of `issue[0].diagnostics`, split on `": "`. There is no `details` object and no
`text`. These are the failures you will see while standing up a key:

<StatusBadge code="401" /> `HPB-00106` — auth failed. The token is missing, malformed, or
revoked. Confirm the header reads `Authorization: Bearer <token>` with a single space, that
you pasted the full secret from the reveal screen, and that the key still shows as active in
the list. After a revoke (step 6) this is the expected response for the old key.

<StatusBadge code="403" /> `HPB-00104` — insufficient scope. The token authenticated but
lacks the scope the request needs — for the step 5 search, <Scope name="system/Patient.rs" />.
The partner is calling a resource you did not grant. Re-mint the key with the right scopes,
or confirm the partner is calling only what you granted.

<StatusBadge code="400" /> `HPB-00101` — validation error. A request parameter is malformed —
most often an un-encoded accent in the `name` search. Encode `á` as `%C3%A1`.

<StatusBadge code="429" /> `HPB-00105` — rate limited. The key exceeded its request budget.
The response carries a `Retry-After` header; wait that many seconds before retrying. A
partner that trips this constantly is polling too aggressively — a workflow conversation,
not a key problem.

A representative `403` body:

```json
{
  "resourceType": "OperationOutcome",
  "issue": [
    {
      "severity": "error",
      "code": "forbidden",
      "diagnostics": "HPB-00104: Insufficient scope"
    }
  ]
}
```

<Callout variant="note">
A second `401`, `HPB-00107` (auth expired), applies to the short-lived access tokens issued
by SMART Backend Services, not to an admin bearer token. If a partner sees it while using
the key you minted here, they are sending a SMART access token instead of your admin bearer
token — a sign they wired up the wrong auth mode.
</Callout>

## Next recipes

- **Run your first authenticated Patient search** — read the `searchset` Bundle your
  verification request returns and recognize the four first-run failures in depth.
- **Authenticate as a SMART Backend Services client** — when a partner needs short-lived,
  key-signed tokens (`client_credentials` + `private_key_jwt`, RS384) instead of a
  long-lived admin bearer token.
- **Create and update a Patient** — move a partner from <Scope name="system/Patient.rs" />
  to <Scope name="system/Patient.cru" /> and POST/PUT patient records.
- **Write an Observation with a Provenance trail** — pair
  <Scope name="system/Observation.cru" /> with <Scope name="system/Provenance.c" /> so
  every write carries its audit record.
