Your first Phenostore resources
Create one synthetic Patient, read it by its server-assigned ID, and find it by identifier. Phenostore is in beta; request access to get your onboarding values.
Before you start
Use a server-side shell with Bash, curl, and jq. Onboarding supplies five values:
| Value | Environment variable | Purpose |
|---|---|---|
| API URL | PHENOSTORE_BASE_URL | API origin, without a trailing slash or tenant/store suffix |
| Tenant ID | TENANT_ID | Your tenant |
| Store ID | STORE_ID | Your store within that tenant |
| Client ID | PHENOSTORE_CLIENT_ID | OAuth client authorized for the store |
| Client secret | PHENOSTORE_CLIENT_SECRET | Secret used to acquire access tokens |
Set these environment variables to the supplied values before continuing.
These are server-side credentials: keep the secret and tokens out of browser
code and source control. Leave shell tracing (set -x) disabled.
The examples use synthetic data only.
The API origin and store's FHIR base are different:
FHIR_BASE="${PHENOSTORE_BASE_URL}/v1/tenants/${TENANT_ID}/stores/${STORE_ID}"
Authenticate
Exchange credentials at /oauth/token relative to the API origin, using a
form-encoded body. This captures the access token without printing it:
set -euo pipefail
ACCESS_TOKEN="$(curl --fail --silent --show-error \
--request POST "${PHENOSTORE_BASE_URL}/oauth/token" \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode "client_id=${PHENOSTORE_CLIENT_ID}" \
--data-urlencode "client_secret=${PHENOSTORE_CLIENT_SECRET}" \
| jq -er '.access_token | select(type == "string" and length > 0)')"
Send Authorization: Bearer with that token on subsequent requests. The token
response includes expires_in; acquire another token when it expires. Use the
Phenostore origin supplied during onboarding for this exchange.
Create a Patient
POST a resource document to /Patient under the FHIR base. Capture the response
and extract its server-assigned id for the read request:
CREATED="$(curl --fail --silent --show-error \
--request POST "${FHIR_BASE}/Patient" \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--header 'Content-Type: application/fhir+json' \
--data-binary @- <<'JSON'
{
"resourceType": "Patient",
"identifier": [
{"system": "https://example.com/patients", "value": "beta-demo-001"}
],
"name": [{"family": "Example", "given": ["Beta"]}]
}
JSON
)"
PATIENT_ID="$(printf '%s' "$CREATED" | jq -er '.id | select(type == "string" and length > 0)')"
A successful create returns HTTP 201 and the resource document. This response
is illustrative; the server supplies id, meta.versionId, and meta.lastUpdated:
{
"resourceType": "Patient",
"id": "019a456789ab7cde8f0123456789abcd",
"meta": {"versionId": "1", "lastUpdated": "2026-11-03T12:00:00Z"},
"identifier": [
{"system": "https://example.com/patients", "value": "beta-demo-001"}
],
"name": [{"family": "Example", "given": ["Beta"]}]
}
Read the Patient
Use the ID captured from the create response:
curl --fail --silent --show-error \
"${FHIR_BASE}/Patient/${PATIENT_ID}" \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--header 'Accept: application/fhir+json' | jq .
Read returns the resource document directly, without a platform response envelope.
Search by identifier
Pass a token search value containing the identifier system and value, separated
by |. Let curl URL-encode it:
curl --fail --silent --show-error --get \
"${FHIR_BASE}/Patient" \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--header 'Accept: application/fhir+json' \
--data-urlencode 'identifier=https://example.com/patients|beta-demo-001' | jq .
Search returns a Bundle with type: "searchset". Matching resources appear in
entry[].resource; an empty result may omit entry. Bundle.total is not
always returned. If link includes an item with relation: "next", follow its
url with the same authorization to retrieve the next page. Repeating the
create step creates another resource, so this identifier may match more than one Patient.
Common routes
All paths below are relative to the store's FHIR base. Only /oauth/token
is relative to the API origin.
| Operation | Method | Path |
|---|---|---|
| Create | POST | /{resourceType} |
| Read | GET | /{resourceType}/{id} |
| Update or upsert | PUT | /{resourceType}/{id} |
| Delete | DELETE | /{resourceType}/{id} |
| Search | GET | /{resourceType} |
| Resource history | GET | /{resourceType}/{id}/_history |
| Transaction or batch Bundle | POST | FHIR base itself (no additional path) |
| Capabilities | GET | /metadata |
The authenticated /metadata response is a CapabilityStatement describing
supported resource types, interactions, and search parameters.
Errors and next steps
A 401 usually means invalid client credentials at token exchange, or a missing, invalid, or expired Bearer token on a FHIR request. A 403 means the client does not have permission for the requested tenant, store, or operation.
FHIR errors use an OperationOutcome document with an issue array describing
the failure. Token exchange errors use OAuth error fields instead. The curl
examples fail on HTTP errors; when debugging, inspect response bodies locally
and sanitize them before sharing or logging them.
Continue with the Phenostore SDK guide for Go and Python. For onboarding, request beta access.