Konko FHIR API
FHIR API
api.medplum.konko.aiFHIR R4
API reference · FHIR R4

Konko FHIR API

Read the patients, practitioners, services, locations and appointments that Konko manages for your organization, and receive a webhook whenever one of them changes. The API implements the HL7 FHIR R4 standard, so standard FHIR tools and libraries work with it.

Base URLhttps://api.medplum.konko.ai/fhir/R4
StandardHL7 FHIR R4 (4.0.1)
AuthenticationOAuth 2.0 client credentials
Formatapplication/fhir+json

What your API client can reach

ResourceWhat it holdsAccess
PatientPeople who have booked or attended an appointment.Read
PractitionerDoctors and other professionals.Read
PractitionerRoleA practitioner at one location, with their services. Appointments point here.Read
LocationSites where patients are seen, with opening hours.Read
HealthcareServiceServices patients can book, with prices and instructions.Read
SlotBusy time on a practitioner's calendar: appointments, blocks and holidays.Read
AppointmentBooked visits from your EHR, calendars and Konko's assistant.Read
SubscriptionYour webhooks.Read and write
AuditEventDelivery attempts of your webhooks.Read
Get started

Quickstart

Konko creates an API client for your organization and sends you its client ID and secret through a secure channel. With those, three requests get you from nothing to live updates.

  1. Get an access token

    Exchange your client ID and secret for a token at POST /oauth2/token. It's valid for an hour. See Authentication.

  2. Read a day of appointments

    Search appointments by date and include each patient in the same response. Dates carry the clinic's UTC offset. See Appointment.

  3. Subscribe to changes

    Create a subscription with your HTTPS endpoint and a signing secret. From now on, every appointment change is sent to you. See Webhooks.

POST/oauth2/token
curl "https://api.medplum.konko.ai/oauth2/token" \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  --data-urlencode "grant_type=client_credentials"
GET/fhir/R4/Appointment
curl -G "https://api.medplum.konko.ai/fhir/R4/Appointment" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  --data-urlencode "date=ge2026-10-13T00:00:00-06:00" \
  --data-urlencode "date=lt2026-10-14T00:00:00-06:00" \
  --data-urlencode "status=booked" \
  --data-urlencode "_include=Appointment:patient" \
  --data-urlencode "_sort=date"
POST/fhir/R4/Subscription
curl -X POST "https://api.medplum.konko.ai/fhir/R4/Subscription" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/fhir+json" \
  -d '{
  "resourceType": "Subscription",
  "status": "active",
  "reason": "Appointment changes for our CRM",
  "criteria": "Appointment",
  "channel": {
    "type": "rest-hook",
    "endpoint": "https://integraciones.clinicahorizonte.example/konko/webhooks",
    "payload": "application/fhir+json"
  },
  "extension": [
    {
      "url": "https://www.medplum.com/fhir/StructureDefinition/subscription-secret",
      "valueString": "whsec_7b1d4e9a2c6f8b3e5a0d9c7f1e4b6a28"
    }
  ]
}'
Get started

Authentication

The API uses the OAuth 2.0 client credentials flow. Konko gives your organization an API client with a client ID and a client secret. Exchange them for an access token and send the token with every request.

Request an access token

POST/oauth2/token

Exchanges your client credentials for an access token. Send the body form-encoded (application/x-www-form-urlencoded); JSON bodies are rejected.

Body parameters
  • grant_typestringRequired

    Always client_credentials.

  • client_idstring

    Your client ID. Required unless you send the credentials as HTTP Basic (Authorization: Basic base64(client_id:client_secret)), which is what the cURL example does.

  • client_secretstring

    Your client secret. Required unless you send it as HTTP Basic.

Response
  • access_tokenstring

    A signed JWT. Send it as Authorization: Bearer <access_token>.

  • token_typestring

    Always Bearer.

  • expires_ininteger

    Seconds until the token expires. Normally 3600 (one hour).

  • scopestring

    Always openid.

  • projectReference

    Your organization's project. Every request made with this token is limited to it.

  • profileReference

    Your API client, as ClientApplication/<client_id>.

  • id_tokenstring

    An OpenID Connect ID token. You don't need it for API calls.

There is no refresh token. When a token expires, request a new one the same way.

POST/oauth2/token
curl "https://api.medplum.konko.ai/oauth2/token" \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  --data-urlencode "grant_type=client_credentials"
200 OK  Response
{
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid",
  "id_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6IjRmM2E…",
  "access_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6IjRmM2E4YjE2In0.eyJjbGllbnRfaWQiOiI3YzFlOWI1Mi…",
  "project": {
    "reference": "Project/0f5c3d2a-7b1e-4c8f-9a6d-2e4b8c1f7a90"
  },
  "profile": {
    "reference": "ClientApplication/7c1e9b52-3a4d-4f8e-b6c2-91d0a5e3f7b4"
  }
}

Send the token

Pass the token in the Authorization header of every API request.

Call the API from your servers. It doesn't accept cross-origin requests from web browsers, which also keeps your credentials out of front-end code.

Reuse tokens until they expire

  • A token is valid for expires_in seconds, one hour by default. Keep it in memory and share it across threads or workers.
  • Request a new token about a minute before the old one expires, or when a request returns 401 Unauthorized. Then retry that request once.
  • Don't request a token per API call. Token requests have their own, much lower rate limit.
Note

The API also accepts your client ID and secret as HTTP Basic credentials on each request. Use that only for quick manual tests: it sends your secret with every call instead of a short-lived token.

GET/fhir/R4/Practitioner
curl -G "https://api.medplum.konko.ai/fhir/R4/Practitioner" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  --data-urlencode "active=true"
Token helper
import time
import requests

TOKEN_URL = "https://api.medplum.konko.ai/oauth2/token"


class KonkoToken:
    """Caches one access token and renews it shortly before it expires."""

    def __init__(self, client_id: str, client_secret: str):
        self._auth = (client_id, client_secret)
        self._token: str | None = None
        self._expires_at = 0.0

    def get(self) -> str:
        if self._token is None or time.time() > self._expires_at - 60:
            resp = requests.post(
                TOKEN_URL,
                data={"grant_type": "client_credentials"},
                auth=self._auth,  # HTTP Basic: client_id / client_secret
                timeout=30,
            )
            resp.raise_for_status()
            body = resp.json()
            self._token = body["access_token"]
            self._expires_at = time.time() + body["expires_in"]
        return self._token

    def invalidate(self) -> None:
        self._token = None  # call after a 401, then retry once

Keep credentials safe

  • Store the client secret in a secrets manager and use it only from your servers. Never put it in a browser, mobile app or public repository.
  • If a secret leaks, tell us right away. We can disable the client and revoke its active tokens.
Token errors
Statuserrorerror_descriptionWhat to check
400invalid_requestMissing grant_typeSend grant_type=client_credentials.
400invalid_requestUnsupported grant_typeOnly client_credentials is available to API clients.
400invalid_requestMissing client_id / Missing client_secretSend both, in the body or as HTTP Basic.
400invalid_requestInvalid clientThe client ID is wrong, or the client has been disabled.
400invalid_requestInvalid secretThe secret is wrong.
400(plain text)Unsupported content typeUse application/x-www-form-urlencoded.
429OperationOutcomeToo Many RequestsMore than 160 token requests in a minute. Cache your token.
Get started

Access and permissions

Your API client can read the resources below and manage its own webhooks.

ResourceAllowed
Patient, Practitioner, PractitionerRole, Location, HealthcareService, Slot, AppointmentRead, search, version history
SubscriptionCreate, read, update, delete and search your own webhooks
AuditEventRead and search webhook delivery attempts
Get started

Rate limits

Two limits protect the API: a request rate per IP address, and a quota of FHIR interaction points per API client. Both reset every minute.

LimitCounted perAllowance per minute
Token requests (/oauth2/*)Client IP address160 requests
All other requestsClient IP address60,000 requests
FHIR interaction quotaAPI client50,000 points

How interactions are counted

Each interaction spends points from your client's per-minute quota. Batch requests spend points for every entry they contain.

InteractionPoints
Read or version read (GET /Patient/{id})1
Each resource returned by _include1
Search, including one page of results (GET /Appointment?…)20
History (GET /Patient/{id}/_history)10
Create, update, patch or delete100

Reading your remaining quota

Every response carries RateLimit headers, one per limit that applies. r is what remains in the current window and t is the number of seconds until the window resets. Some HTTP clients join the two headers into one value separated by a comma.

When you hit a limit

The API responds 429 Too Many Requests with an OperationOutcome. There is no Retry-After header: wait for the t seconds reported in RateLimit, then resume. If the header is missing, back off exponentially with jitter, starting at one second.

Stay well inside the limits

  • Use webhooks instead of polling for changes.
  • Cache directory data (practitioners, services and locations). It changes rarely, and a webhook tells you when it does.
  • Fetch related resources with _include in the same search rather than reading them one by one.
  • Request larger pages, for example _count=200, when you read many resources.

Other limits

LimitValueWhat to do instead
Request body1 MBSplit large batches.
Results per page (_count)20 by default, up to 1,000Follow next links.
Offset pagination (_offset)Up to 10,000Sort by _lastUpdated to get cursor links with no cap. See Pagination.
Request duration30 secondsNarrow the search with filters or a smaller _count.
URL and headersAbout 16 KBSend long searches as POST …/_search.
Rate limit headers
HTTP/1.1 200 OK
Content-Type: application/fhir+json; charset=utf-8
RateLimit: "requests";r=59987;t=41
RateLimit: "fhirInteractions";r=49740;t=41
429 Too Many Requests  Response
{
  "resourceType": "OperationOutcome",
  "id": "too-many-requests",
  "issue": [
    {
      "severity": "error",
      "code": "throttled",
      "details": {
        "text": "Too Many Requests"
      },
      "diagnostics": "{\"_remainingPoints\":0,\"_msBeforeNext\":37210,\"_consumedPoints\":50020,\"_isFirstInDuration\":false,\"limit\":50000}"
    }
  ]
}
Get started

Errors

Failed requests return an HTTP status code and a FHIR OperationOutcome that explains what went wrong. The token endpoint is the exception: it answers with OAuth-style error and error_description fields.

StatusMeaning
200 OKThe request succeeded.
201 CreatedA subscription was created. The Location header holds its URL.
400 Bad RequestThe request is malformed: unknown search parameter, invalid value, bad JSON or missing required fields. details.text says which.
401 UnauthorizedThe token is missing, invalid or expired. Get a new token and retry once.
403 ForbiddenYour client isn't allowed to do this. See Access and permissions.
404 Not FoundNo resource with this id exists in your project.
410 GoneThe resource existed but has been deleted.
412 Precondition FailedAn If-Match version check failed on an update.
422 Unprocessable EntityThe request is well formed but breaks a business rule.
429 Too Many RequestsA rate limit was exceeded. See Rate limits.
500, 502, 503, 504A problem on Konko's side. Retry with exponential backoff.

Reading an OperationOutcome

  • issue[].code is a machine-readable category such as invalid, login, forbidden, not-found, deleted or throttled.
  • issue[].details.text is a human-readable explanation. Log it, but don't match on its wording, which can change.
  • The tracing extension carries a requestId and traceId. Include them when you report a problem to Konko.

Correlating requests

Send your own UUID in an X-Trace-Id header, or a W3C traceparent header, and Konko uses it as the trace ID for that request. It then appears in the tracing extension of any error response, and Konko can find the request by it.

When to retry

  • Retry 429 and 5xx responses with exponential backoff and jitter.
  • Retry a 401 once, after requesting a new token.
  • Don't retry other 4xx responses unchanged. Fix the request first.
404 Not Found  Response
{
  "resourceType": "OperationOutcome",
  "id": "not-found",
  "issue": [
    {
      "severity": "error",
      "code": "not-found",
      "details": {
        "text": "Not found"
      }
    }
  ],
  "extension": [
    {
      "url": "https://medplum.com/fhir/StructureDefinition/tracing",
      "extension": [
        {
          "url": "requestId",
          "valueId": "7d2f9b41-6c3e-4a8d-b1f5-0e9a2c7d4b63"
        },
        {
          "url": "traceId",
          "valueId": "c3a8e1f6-9b2d-4e7a-8c5f-1d0b6e9a3f27"
        }
      ]
    }
  ]
}
400 Bad Request  Response
{
  "resourceType": "OperationOutcome",
  "issue": [
    {
      "severity": "error",
      "code": "invalid",
      "details": {
        "text": "Unknown search parameter: doctor"
      }
    }
  ]
}
401 Unauthorized  Response
{
  "resourceType": "OperationOutcome",
  "id": "unauthorized",
  "issue": [
    {
      "severity": "error",
      "code": "login",
      "details": {
        "text": "Unauthorized"
      }
    }
  ]
}
Working with FHIR

How the resources fit together

Resources refer to each other with references such as {"reference": "PractitionerRole/f7c3…"}. The PractitionerRole sits in the middle: a practitioner working at one location.

Patientthe personPractitionerthe doctorAppointmenta booked visitPractitionerRoledoctor at a locationLocationa siteSlotbusy timeHealthcareServicewhat can be bookedparticipant.actorparticipant.actorslotpractitionerlocationhealthcareServicelocation
Arrows point from the resource that holds a reference to the resource it references, labelled with the field. Appointments reach the doctor, the location and the services only through the PractitionerRole.
To getFollowIn one request
Patient of an appointmentparticipant.actor where the reference starts with Patient/_include=Appointment:patient
Doctor of an appointmentparticipant.actor (PractitionerRole), then PractitionerRole.practitioner_include=Appointment:actor&_include:iterate=PractitionerRole:practitioner
Location of an appointmentThe PractitionerRole's location_include=Appointment:actor&_include:iterate=PractitionerRole:location
Busy slot of an appointmentslot_include=Appointment:slot
Services a doctor offers at a sitePractitionerRole.healthcareService_include=PractitionerRole:service
Working with FHIR

Searching

Every resource type can be searched with GET /fhir/R4/{ResourceType}?parameter=value. Each resource's reference lists the parameters it supports.

Combining parameters

  • Different parameters must all match: status=booked&patient=Patient/{id}.
  • Commas mean any of: status=booked,pending.
  • Repeat a parameter to require both conditions, which is how you write a range: date=ge2026-10-13&date=lt2026-10-20.
  • :missing=true finds resources without the field; :not excludes a value.

Parameter types

TypeHow it matchesExample
tokenAn exact code or ID. system|value pins the system, value alone matches any system, system| matches any value in that system. :text searches the display text instead.status=booked
stringCase-insensitive, from the start of the value. :contains matches anywhere, :exact must match exactly. Accents count: jimenez doesn't match “Jiménez”.name:contains=gineco
name and addressEvery word you send must start a word of the name or address, in any order. name=ana mora finds “Ana Lucía Mora Vargas”.name=ana mora
referenceType/id of the referenced resource.patient=Patient/3c9f6a1e…
dateAn ISO 8601 date or date-time, with an optional prefix. A date without a time covers the whole day. Always include the UTC offset in date-times.date=ge2026-10-13T00:00:00-06:00

Date prefixes

PrefixMeaning
eq (default)Equal, or within the day or period you give.
neNot equal.
gt, geAfter, or on or after.
lt, leBefore, or on or before.
sa, ebStarts after, ends before.
apApproximately.

Chained search

Filter on a field of a referenced resource by chaining parameters with a dot. Name the target type when a reference can point to several: actor:PractitionerRole.practitioner=Practitioner/{id} finds a doctor's appointments at every location. Chains can be up to three links long, and _has filters in the opposite direction.

Long queries

URLs and headers are limited to about 16 KB. For long filters, such as many IDs, send the same parameters form-encoded to POST /fhir/R4/{ResourceType}/_search. The result is identical.

An unknown parameter fails with 400 Bad Request and the message Unknown search parameter: …. Parameters are never silently ignored.

GET/fhir/R4/Appointment
curl -G "https://api.medplum.konko.ai/fhir/R4/Appointment" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  --data-urlencode "actor:PractitionerRole.practitioner=Practitioner/d1b5e8c3-4f7a-4d2e-9b6c-8a0f3e5d1c72" \
  --data-urlencode "date=ge2026-10-13T00:00:00-06:00" \
  --data-urlencode "date=lt2026-10-20T00:00:00-06:00" \
  --data-urlencode "status=booked,pending" \
  --data-urlencode "_sort=date"
POST/fhir/R4/Patient/_search
curl -X POST "https://api.medplum.konko.ai/fhir/R4/Patient/_search" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "_id=3c9f6a1e-8d4b-4f2a-b5e7-0d9c2f8a6b13,8f2d5b9a-1e6c-4d3f-a7b8-5c0e9d2f4a61" \
  --data-urlencode "_elements=name,telecom,birthDate"
Common parameters
  • _idtoken

    One or more resource IDs, comma-separated.

  • _lastUpdateddate

    When the resource last changed.

    Example: _lastUpdated=gt2026-10-01T00:00:00Z
  • _countinteger

    Results per page, 1 to 1,000. Default 20.

  • _sortstring

    Comma-separated sort keys; a leading - sorts descending.

    Example: _sort=-date
  • _elementsstring

    Return only these top-level fields, plus id and the required ones.

    Example: _elements=status,start,end,participant
  • _summarycode

    count returns only the number of matches. true returns summary fields only.

  • _totalcode

    accurate or estimate adds total to the Bundle. Without it there is no total, which keeps searches fast.

  • _include, _revincludestring

    Return referenced or referencing resources in the same Bundle. See Related resources.

Working with FHIR

Pagination

Searches return a searchset Bundle holding one page of results. Follow its next link until there isn't one.

  • Each result is an entry with the resource, its fullUrl, and search.mode: match for results, include for resources added by _include or _revinclude.
  • Pages hold 20 results by default. Ask for up to 1,000 with _count.
  • link lists self, first and, when more results exist, next. Request the next URL exactly as given, with your token. Don't build page URLs yourself.
  • By default pages are numbered with _offset, which stops at 10,000 results with 400 Search offset exceeds maximum.
  • To read further, sort by _lastUpdated ascending and nothing else, with _count of at least 20. The next links then use a _cursor and have no limit. This is the right way to export a whole resource type.
  • Bundles carry no total unless you ask for _total=accurate. For a count alone, use _summary=count.
Note

A next link repeats your whole query. If the query is long, the link can exceed the URL limit and fail with 431. Send its query string as a form body to POST …/_search instead.

Read every page
import requests

def fetch_all(url: str, params: dict, token: str):
    """Yields every matching resource, following the server's next links."""
    headers = {"Authorization": f"Bearer {token}"}
    resp = requests.get(url, params=params, headers=headers, timeout=30)
    while True:
        resp.raise_for_status()
        bundle = resp.json()
        for entry in bundle.get("entry", []):
            if entry.get("search", {}).get("mode", "match") == "match":
                yield entry["resource"]
        next_url = next((l["url"] for l in bundle.get("link", []) if l["relation"] == "next"), None)
        if not next_url:
            return
        resp = requests.get(next_url, headers=headers, timeout=30)


for appt in fetch_all(
    "https://api.medplum.konko.ai/fhir/R4/Appointment",
    {"_lastUpdated": "gt2026-10-01T00:00:00Z", "_sort": "_lastUpdated", "_count": 200},
    access_token,
):
    upsert(appt)
Cursor links
{
  "resourceType": "Bundle",
  "type": "searchset",
  "entry": [],
  "link": [
    {
      "relation": "self",
      "url": "https://api.medplum.konko.ai/fhir/R4/Appointment?_count=200&_lastUpdated=gt2026-10-01T00%3A00%3A00Z&_sort=_lastUpdated"
    },
    {
      "relation": "first",
      "url": "https://api.medplum.konko.ai/fhir/R4/Appointment?_count=200&_lastUpdated=gt2026-10-01T00%3A00%3A00Z&_sort=_lastUpdated"
    },
    {
      "relation": "next",
      "url": "https://api.medplum.konko.ai/fhir/R4/Appointment?_count=200&_cursor=2-1759763347382-&_lastUpdated=gt2026-10-01T00%3A00%3A00Z&_sort=_lastUpdated"
    }
  ]
}
Working with FHIR

Related resources

Fetch the resources a result points to, or the ones that point to it, in the same request.

  • _include=Appointment:patient adds the patient referenced by each appointment. The value is SourceType:search-parameter.
  • _include:iterate follows references from resources that were themselves included: _include=Appointment:actor&_include:iterate=PractitionerRole:practitioner returns appointments, their PractitionerRoles and those roles' practitioners.
  • _revinclude works the other way round and adds resources that reference the results: GET /Practitioner?_revinclude=PractitionerRole:practitioner returns practitioners with all their roles.
  • Included resources come after the matches on the same page, with search.mode set to include.
  • Each included resource costs 1 point of your quota, far less than reading it separately.
GET/fhir/R4/PractitionerRole
curl -G "https://api.medplum.konko.ai/fhir/R4/PractitionerRole" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  --data-urlencode "location=Location/e4a9c1d7-2b6f-4a3e-9c8d-5f1b7e0a2c64" \
  --data-urlencode "_include=PractitionerRole:practitioner" \
  --data-urlencode "_include=PractitionerRole:service"
Working with FHIR

Common tasks

Requests for the questions integrations ask most often. Paths are relative to /fhir/R4; URL-encode values when you send them.

TaskRequest
A patient's upcoming appointmentsGET /Appointment?patient=Patient/3c9f6a1e…&date=ge2026-10-05T00:00:00-06:00&status=booked&_sort=date
Today's agenda at one locationGET /Appointment?actor:PractitionerRole.location=Location/e4a9c1d7…&date=ge2026-10-13T00:00:00-06:00&date=lt2026-10-14T00:00:00-06:00&_include=Appointment:patient&_sort=date
A doctor's week across all locationsGET /Appointment?actor:PractitionerRole.practitioner=Practitioner/d1b5e8c3…&date=ge2026-10-12&date=lt2026-10-19
Doctors at a location, with their servicesGET /PractitionerRole?location=Location/e4a9c1d7…&active=true&_include=PractitionerRole:practitioner&_include=PractitionerRole:service
Time blocked by the clinic this weekGET /Slot?status=busy-unavailable&start=ge2026-10-12T00:00:00-06:00&start=lt2026-10-19T00:00:00-06:00
Find a patient by EHR file numberGET /Patient?identifier=https://konko.ai/huli/1/patient-file|10
Find a patient by phoneGET /Patient?phone=87654321,+50687654321
Cancellations since yesterdayGET /Appointment?status=cancelled&_lastUpdated=gt2026-10-12T00:00:00-06:00
Bookable servicesGET /HealthcareService?active=true&_elements=name,type,location,extension
Count this month's no-showsGET /Appointment?status=noshow&date=ge2026-10-01&date=lt2026-11-01&_summary=count
Working with FHIR

Version history

Every change creates a new version. You can list a resource's versions or read an old one, for any resource type your client can read.

List a resource's versions

GET/fhir/R4/{type}/{id}/_history

Returns every stored version of one resource, newest first, as a history Bundle. Each entry is one version; a version where the resource was deleted has no resource and an outcome that says Deleted on ….

Parameters
  • ididRequired

    The resource id (path).

  • _countinteger

    Versions per page, up to 1,000. Default 100.

  • _offsetinteger

    Number of versions to skip. History Bundles have no next link; page with _offset and stop when total is reached.

History is available per resource only. There is no history across a whole type; search with _lastUpdated instead.

GET/fhir/R4/Appointment/{id}/_history
curl "https://api.medplum.konko.ai/fhir/R4/Appointment/{appointment_id}/_history" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
200 OK  Response
{
  "resourceType": "Bundle",
  "type": "history",
  "entry": [
    {
      "fullUrl": "https://api.medplum.konko.ai/fhir/R4/Appointment/6b1f8e3d-2a9c-4b5e-8f7d-3e0a6c1b9d84",
      "request": {
        "method": "GET",
        "url": "Appointment/6b1f8e3d-2a9c-4b5e-8f7d-3e0a6c1b9d84/_history/0d6b3f9e-4a1c-4e7b-8f2d-9c5a1e7b3d60"
      },
      "response": {
        "status": "200",
        "outcome": {
          "resourceType": "OperationOutcome",
          "id": "ok",
          "issue": [
            {
              "severity": "information",
              "code": "informational",
              "details": {
                "text": "All OK"
              }
            }
          ]
        }
      },
      "resource": {
        "resourceType": "Appointment",
        "id": "6b1f8e3d-2a9c-4b5e-8f7d-3e0a6c1b9d84",
        "meta": {
          "versionId": "0d6b3f9e-4a1c-4e7b-8f2d-9c5a1e7b3d60",
          "lastUpdated": "2026-10-09T16:20:02.841Z"
        },
        "identifier": [
          {
            "system": "https://konko.ai/huli/appointment",
            "value": "100"
          }
        ],
        "status": "cancelled",
        "description": "Consulta de Ginecología — Ana Lucía Mora Vargas",
        "start": "2026-10-13T08:30:00-06:00",
        "end": "2026-10-13T09:00:00-06:00",
        "slot": [
          {
            "reference": "Slot/5e8b1d4f-7a2c-4e9d-8b3f-1c6a0e9d7b26"
          }
        ],
        "participant": [
          {
            "actor": {
              "reference": "PractitionerRole/f7c3a9e5-1b8d-4e6f-a2c4-9d7b0e3f5a18"
            },
            "status": "tentative"
          },
          {
            "actor": {
              "reference": "Patient/3c9f6a1e-8d4b-4f2a-b5e7-0d9c2f8a6b13"
            },
            "status": "accepted"
          }
        ]
      }
    },
    {
      "fullUrl": "https://api.medplum.konko.ai/fhir/R4/Appointment/6b1f8e3d-2a9c-4b5e-8f7d-3e0a6c1b9d84",
      "request": {
        "method": "GET",
        "url": "Appointment/6b1f8e3d-2a9c-4b5e-8f7d-3e0a6c1b9d84/_history/a8f1c4e7-2b9d-4e6a-9c3f-5d0b8e2a7c14"
      },
      "response": {
        "status": "200",
        "outcome": {
          "resourceType": "OperationOutcome",
          "id": "ok",
          "issue": [
            {
              "severity": "information",
              "code": "informational",
              "details": {
                "text": "All OK"
              }
            }
          ]
        }
      },
      "resource": {
        "resourceType": "Appointment",
        "id": "6b1f8e3d-2a9c-4b5e-8f7d-3e0a6c1b9d84",
        "meta": {
          "versionId": "a8f1c4e7-2b9d-4e6a-9c3f-5d0b8e2a7c14",
          "lastUpdated": "2026-10-05T15:21:47.382Z"
        },
        "identifier": [
          {
            "system": "https://konko.ai/huli/appointment",
            "value": "100"
          }
        ],
        "status": "booked",
        "description": "Consulta de Ginecología — Ana Lucía Mora Vargas",
        "start": "2026-10-13T08:30:00-06:00",
        "end": "2026-10-13T09:00:00-06:00",
        "slot": [
          {
            "reference": "Slot/5e8b1d4f-7a2c-4e9d-8b3f-1c6a0e9d7b26"
          }
        ],
        "participant": [
          {
            "actor": {
              "reference": "PractitionerRole/f7c3a9e5-1b8d-4e6f-a2c4-9d7b0e3f5a18"
            },
            "status": "tentative"
          },
          {
            "actor": {
              "reference": "Patient/3c9f6a1e-8d4b-4f2a-b5e7-0d9c2f8a6b13"
            },
            "status": "accepted"
          }
        ]
      }
    }
  ],
  "total": 2
}

Read one version

GET/fhir/R4/{type}/{id}/_history/{vid}

Returns one specific version of a resource, by the versionId found in meta or in a history entry.

Path parameters
  • ididRequired

    The resource id.

  • vididRequired

    The versionId.

GET/fhir/R4/Appointment/{id}/_history/{vid}
curl "https://api.medplum.konko.ai/fhir/R4/Appointment/{appointment_id}/_history/{version_id}" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Working with FHIR

Batch requests

Combine several reads and searches in one round trip.

Send a batch

POST/fhir/R4

Sends several reads and searches in one HTTP request. Post a Bundle of type batch whose entries each have a request with method: GET and a URL relative to /fhir/R4.

The response is a batch-response Bundle with one entry per request, in the same order. Each entry has its own response.status, so one failed entry doesn't fail the others.

  • Every entry counts against your quota as if you had sent it separately.
  • The request body is limited to 1 MB.
  • Entries are processed independently, not in the order you list them.
POST/fhir/R4
curl -X POST "https://api.medplum.konko.ai/fhir/R4" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/fhir+json" \
  -d '{
  "resourceType": "Bundle",
  "type": "batch",
  "entry": [
    {
      "request": {
        "method": "GET",
        "url": "Patient/3c9f6a1e-8d4b-4f2a-b5e7-0d9c2f8a6b13"
      }
    },
    {
      "request": {
        "method": "GET",
        "url": "PractitionerRole/f7c3a9e5-1b8d-4e6f-a2c4-9d7b0e3f5a18"
      }
    },
    {
      "request": {
        "method": "GET",
        "url": "Appointment?patient=Patient/3c9f6a1e-8d4b-4f2a-b5e7-0d9c2f8a6b13&date=ge2026-10-01&_count=10"
      }
    }
  ]
}'
200 OK  Response
{
  "resourceType": "Bundle",
  "type": "batch-response",
  "entry": [
    {
      "response": {
        "outcome": {
          "resourceType": "OperationOutcome",
          "id": "ok",
          "issue": [
            {
              "severity": "information",
              "code": "informational",
              "details": {
                "text": "All OK"
              }
            }
          ]
        },
        "status": "200",
        "location": "Patient/3c9f6a1e-8d4b-4f2a-b5e7-0d9c2f8a6b13"
      },
      "resource": {
        "resourceType": "Patient",
        "id": "3c9f6a1e-8d4b-4f2a-b5e7-0d9c2f8a6b13",
        "meta": {
          "versionId": "6e3b9f1d-8a5c-4d2e-b7f4-1c6a9e3d8b50",
          "lastUpdated": "2026-10-05T15:21:40.964Z"
        },
        "identifier": [
          {
            "system": "https://konko.ai/huli/1/patient-file",
            "value": "10"
          }
        ],
        "active": true,
        "name": [
          {
            "use": "official",
            "family": "Mora Vargas",
            "given": [
              "Ana Lucía"
            ]
          }
        ],
        "telecom": [
          {
            "system": "phone",
            "value": "87654321"
          },
          {
            "system": "email",
            "value": "ana.mora@correo.example"
          }
        ],
        "gender": "female",
        "birthDate": "1990-04-17",
        "address": [
          {
            "text": "Santa Ana, San José"
          }
        ]
      }
    },
    {
      "response": {
        "outcome": {
          "resourceType": "OperationOutcome",
          "id": "ok",
          "issue": [
            {
              "severity": "information",
              "code": "informational",
              "details": {
                "text": "All OK"
              }
            }
          ]
        },
        "status": "200",
        "location": "PractitionerRole/f7c3a9e5-1b8d-4e6f-a2c4-9d7b0e3f5a18"
      },
      "resource": {
        "resourceType": "PractitionerRole",
        "id": "f7c3a9e5-1b8d-4e6f-a2c4-9d7b0e3f5a18",
        "meta": {
          "versionId": "2c7e0a4d-9f3b-4d6a-b1e8-5f2c9a7d0e43",
          "lastUpdated": "2026-09-14T17:02:17.056Z"
        },
        "language": "es",
        "extension": [
          {
            "url": "http://hl7.org/fhir/StructureDefinition/timezone",
            "valueCode": "America/Costa_Rica"
          }
        ],
        "identifier": [
          {
            "system": "https://konko.ai/practitioner-role",
            "value": "DOC001:LOC001"
          }
        ],
        "active": true,
        "practitioner": {
          "reference": "Practitioner/d1b5e8c3-4f7a-4d2e-9b6c-8a0f3e5d1c72"
        },
        "organization": {
          "reference": "Organization/b2d7e4a1-5c3f-4e9b-8a1d-6f2c0e7b9d35"
        },
        "specialty": [
          {
            "text": "Ginecología"
          }
        ],
        "location": [
          {
            "reference": "Location/e4a9c1d7-2b6f-4a3e-9c8d-5f1b7e0a2c64"
          }
        ],
        "healthcareService": [
          {
            "reference": "HealthcareService/a3f6b8d2-9e1c-4b7a-8d5f-2c0e6a9b1d47"
          }
        ]
      }
    },
    {
      "response": {
        "outcome": {
          "resourceType": "OperationOutcome",
          "id": "ok",
          "issue": [
            {
              "severity": "information",
              "code": "informational",
              "details": {
                "text": "All OK"
              }
            }
          ]
        },
        "status": "200"
      },
      "resource": {
        "resourceType": "Bundle",
        "type": "searchset",
        "entry": [
          {
            "fullUrl": "https://api.medplum.konko.ai/fhir/R4/Appointment/6b1f8e3d-2a9c-4b5e-8f7d-3e0a6c1b9d84",
            "resource": {
              "resourceType": "Appointment",
              "id": "6b1f8e3d-2a9c-4b5e-8f7d-3e0a6c1b9d84",
              "meta": {
                "versionId": "a8f1c4e7-2b9d-4e6a-9c3f-5d0b8e2a7c14",
                "lastUpdated": "2026-10-05T15:21:47.382Z"
              },
              "identifier": [
                {
                  "system": "https://konko.ai/huli/appointment",
                  "value": "100"
                }
              ],
              "status": "booked",
              "description": "Consulta de Ginecología — Ana Lucía Mora Vargas",
              "start": "2026-10-13T08:30:00-06:00",
              "end": "2026-10-13T09:00:00-06:00",
              "slot": [
                {
                  "reference": "Slot/5e8b1d4f-7a2c-4e9d-8b3f-1c6a0e9d7b26"
                }
              ],
              "comment": "full_name: Ana Lucía Mora Vargas\nphone_number: +50687654321\nservice: Consulta de Ginecología\nlocation: Sede Escazú",
              "participant": [
                {
                  "actor": {
                    "reference": "PractitionerRole/f7c3a9e5-1b8d-4e6f-a2c4-9d7b0e3f5a18"
                  },
                  "status": "tentative"
                },
                {
                  "actor": {
                    "reference": "Patient/3c9f6a1e-8d4b-4f2a-b5e7-0d9c2f8a6b13"
                  },
                  "status": "accepted"
                }
              ]
            },
            "search": {
              "mode": "match"
            }
          }
        ],
        "link": [
          {
            "relation": "self",
            "url": "https://api.medplum.konko.ai/fhir/R4/Appointment?_count=10&date=ge2026-10-01&patient=Patient%2F3c9f6a1e-8d4b-4f2a-b5e7-0d9c2f8a6b13"
          },
          {
            "relation": "first",
            "url": "https://api.medplum.konko.ai/fhir/R4/Appointment?_count=10&date=ge2026-10-01&patient=Patient%2F3c9f6a1e-8d4b-4f2a-b5e7-0d9c2f8a6b13"
          }
        ]
      }
    }
  ]
}
Working with FHIR

Conventions

The API follows HL7 FHIR R4 (4.0.1). These are the details that matter most when you parse its responses.

TopicConvention
FormatJSON only, as application/fhir+json. Add _pretty=true for indented output while exploring. Responses are gzip-compressed when you send Accept-Encoding: gzip.
IDsEvery resource has a UUID id that never changes. Refer to resources as Type/id.
Timesstart, end and other instants carry the clinic's UTC offset, for example 2026-10-13T08:30:00-06:00. meta.lastUpdated is in UTC. Always parse the offset.
Versionsmeta.versionId changes on every update. Read responses carry it as ETag: W/"<versionId>". Conditional reads (If-None-Match) aren't supported.
LanguageNames, descriptions and instructions are in the clinic's language, usually Spanish. Clinic configuration resources carry language: es.
ExtensionsKonko adds extensions documented on each resource. Ignore extensions you don't recognize, as the FHIR standard requires.
Several sourcesData comes from your EHR, connected calendars, your clinic configuration in Konko and bookings made by Konko's assistant. The same doctor or site can exist more than once; compare identifiers.
Populated fieldsAttributes marked Populated are the ones Konko fills. Other standard FHIR fields are valid but usually empty.
Patients

Patient

A person who has booked or attended an appointment at your clinic.

  • Patients come from three places: your EHR (imported with their patient-file number), calendar invitations (identified by the attendee's email), and bookings made through Konko's assistant (name, phone and birth date as the patient gave them).
  • The same person can exist more than once, for example once from the EHR and once from a calendar invitation. Compare identifiers, phone numbers and birth dates before merging records on your side.
  • Phone numbers are stored as the source provided them. EHR imports often hold local digits (87654321); the assistant stores international format (+50687654321).
AttributesPopulated = filled by Konko
  • idid1..1

    Konko's ID for this resource, a UUID. It never changes.

  • metaMeta1..1

    Version information, maintained by the server.

    2 child attributes
    • versionIdid1..1

      Changes on every update. Use it to detect changes and to ignore duplicate webhook deliveries.

    • lastUpdatedinstant1..1

      When this version was saved, in UTC. Search on it with _lastUpdated.

  • identifierIdentifier0..*Populated

    IDs from the systems this record came from. Match on system and value:

    https://konko.ai/huli/{organizationId}/patient-file: patient-file number in the Huli EHR.

    https://konko.ai/fhir/identifier/google-calendar-patient: email of a calendar-event attendee.

    Patients created by Konko's assistant have no identifier.

  • activeboolean0..1Populated

    true for records imported from an EHR.

  • nameHumanName0..*Populated

    The patient's names.

    5 child attributes
    • usecode0..1

      official for names from an EHR, nickname for a preferred name.

    • textstring0..1

      The full name as one string. Names collected by Konko's assistant have only text.

    • familystring0..1

      Family name, usually both surnames (Mora Vargas).

    • givenstring0..*

      Given names.

    • prefixstring0..*

      Titles such as Dra or Dr.

  • telecomContactPoint0..*Populated

    Phone numbers and emails.

    3 child attributes
    • systemcode0..1

      phone, email or url.

    • valuestring0..1

      The number, address or URL, as stored by the source.

    • usecode0..1

      mobile on numbers collected by Konko's assistant.

  • gendercode0..1Populated

    male, female, other or unknown.

  • birthDatedate0..1Populated

    YYYY-MM-DD.

  • addressAddress0..*Populated

    Usually free text in text.

  • Other FHIR fields

    communication, contact, generalPractitioner, managingOrganization, maritalStatus, deceased[x], photo and link are part of the standard resource. Konko doesn't fill them today, so treat them as optional.

The Patient object
{
  "resourceType": "Patient",
  "id": "3c9f6a1e-8d4b-4f2a-b5e7-0d9c2f8a6b13",
  "meta": {
    "versionId": "6e3b9f1d-8a5c-4d2e-b7f4-1c6a9e3d8b50",
    "lastUpdated": "2026-10-05T15:21:40.964Z"
  },
  "identifier": [
    {
      "system": "https://konko.ai/huli/1/patient-file",
      "value": "10"
    }
  ],
  "active": true,
  "name": [
    {
      "use": "official",
      "family": "Mora Vargas",
      "given": [
        "Ana Lucía"
      ]
    }
  ],
  "telecom": [
    {
      "system": "phone",
      "value": "87654321"
    },
    {
      "system": "email",
      "value": "ana.mora@correo.example"
    }
  ],
  "gender": "female",
  "birthDate": "1990-04-17",
  "address": [
    {
      "text": "Santa Ana, San José"
    }
  ]
}

Retrieve a patient

GET/fhir/R4/Patient/{id}

Returns a patient by its id. The response carries ETag: W/"<versionId>" and Last-Modified headers.

Returns 404 Not Found if no Patient with that id exists in your project, and 410 Gone if it was deleted.

Path parameters
  • ididRequired

    The Patient id.

GET/fhir/R4/Patient/{id}
curl "https://api.medplum.konko.ai/fhir/R4/Patient/{patient_id}" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
200 OK  Response
{
  "resourceType": "Patient",
  "id": "3c9f6a1e-8d4b-4f2a-b5e7-0d9c2f8a6b13",
  "meta": {
    "versionId": "6e3b9f1d-8a5c-4d2e-b7f4-1c6a9e3d8b50",
    "lastUpdated": "2026-10-05T15:21:40.964Z"
  },
  "identifier": [
    {
      "system": "https://konko.ai/huli/1/patient-file",
      "value": "10"
    }
  ],
  "active": true,
  "name": [
    {
      "use": "official",
      "family": "Mora Vargas",
      "given": [
        "Ana Lucía"
      ]
    }
  ],
  "telecom": [
    {
      "system": "phone",
      "value": "87654321"
    },
    {
      "system": "email",
      "value": "ana.mora@correo.example"
    }
  ],
  "gender": "female",
  "birthDate": "1990-04-17",
  "address": [
    {
      "text": "Santa Ana, San José"
    }
  ]
}
Providers

Practitioner

A doctor or other professional who sees patients at your clinic.

  • A practitioner holds the person's details. Where they work and what they offer hang off their PractitionerRole.
  • The same doctor can appear as more than one Practitioner when the data comes from several sources: your EHR, the clinic configuration in Konko, and staff user accounts. Konko's assistant only schedules for practitioners that carry the https://konko.ai/fhir/knowledge-base-id identifier.
AttributesPopulated = filled by Konko
  • idid1..1

    Konko's ID for this resource, a UUID. It never changes.

  • metaMeta1..1

    Version information, maintained by the server.

    2 child attributes
    • versionIdid1..1

      Changes on every update. Use it to detect changes and to ignore duplicate webhook deliveries.

    • lastUpdatedinstant1..1

      When this version was saved, in UTC. Search on it with _lastUpdated.

  • identifierIdentifier0..*Populated

    IDs from the systems this record came from. Match on system and value:

    https://konko.ai/huli/doctor and https://konko.ai/huli/user: doctor and user IDs in the Huli EHR.

    https://konko.ai/practitioner: the practitioner's code in the clinic configuration, such as DOC001.

    https://konko.ai/fhir/knowledge-base-id: the same DOC### code, on the practitioners Konko's assistant books for.

  • activeboolean0..1Populated

    Whether the practitioner is in use.

  • nameHumanName0..*Populated

    The practitioner's name, including titles in prefix or text.

    5 child attributes
    • usecode0..1

      official for names from an EHR, nickname for a preferred name.

    • textstring0..1

      The full name as one string. Names collected by Konko's assistant have only text.

    • familystring0..1

      Family name, usually both surnames (Mora Vargas).

    • givenstring0..*

      Given names.

    • prefixstring0..*

      Titles such as Dra or Dr.

  • telecomContactPoint0..*Populated

    Contact details that apply to all of the practitioner's roles.

    3 child attributes
    • systemcode0..1

      phone, email or url.

    • valuestring0..1

      The number, address or URL, as stored by the source.

    • usecode0..1

      mobile on numbers collected by Konko's assistant.

  • gendercode0..1Populated

    male, female, other or unknown.

  • qualificationBackboneElement0..*Populated

    Licences and specialties from the EHR.

    2 child attributes
    • identifierIdentifier0..*

      https://konko.ai/huli/professional-license for a licence number, https://konko.ai/huli/specialty for a specialty ID.

    • codeCodeableConcept1..1

      The qualification as text only, for example Ginecología y Obstetricia.

  • extensionExtension0..*Populated

    Konko extensions. Ignore any you don't recognize; some are for Konko's internal use.

    1 child attribute
    • biographyvalueString0..1

      url: https://konko.ai/practitioner/biography

      A short biography written for patients.

  • languagecode0..1Populated

    es on resources that come from the clinic's configuration in Konko.

  • Other FHIR fields

    address, birthDate, photo and communication are part of the standard resource. Konko doesn't fill them today, so treat them as optional.

The Practitioner object
{
  "resourceType": "Practitioner",
  "id": "d1b5e8c3-4f7a-4d2e-9b6c-8a0f3e5d1c72",
  "meta": {
    "versionId": "9e2b5d8f-4c1a-4b7e-8d3f-2a6c0e9b5d17",
    "lastUpdated": "2026-09-20T13:44:08.771Z"
  },
  "identifier": [
    {
      "system": "https://konko.ai/huli/doctor",
      "value": "1000"
    },
    {
      "system": "https://konko.ai/huli/user",
      "value": "10000"
    },
    {
      "system": "https://konko.ai/fhir/knowledge-base-id",
      "value": "DOC001"
    }
  ],
  "active": true,
  "name": [
    {
      "use": "official",
      "text": "Dra. Carolina Jiménez Soto",
      "family": "Jiménez Soto",
      "given": [
        "Carolina"
      ]
    }
  ],
  "telecom": [
    {
      "system": "phone",
      "value": "88112233"
    },
    {
      "system": "email",
      "value": "dra.jimenez@clinicahorizonte.example"
    }
  ],
  "gender": "female",
  "qualification": [
    {
      "identifier": [
        {
          "system": "https://konko.ai/huli/professional-license",
          "value": "MED-12345"
        }
      ],
      "code": {
        "text": "Colegio de Médicos y Cirujanos"
      }
    },
    {
      "identifier": [
        {
          "system": "https://konko.ai/huli/specialty",
          "value": "41"
        }
      ],
      "code": {
        "text": "Ginecología y Obstetricia"
      }
    }
  ]
}

Retrieve a practitioner

GET/fhir/R4/Practitioner/{id}

Returns a practitioner by its id. The response carries ETag: W/"<versionId>" and Last-Modified headers.

Returns 404 Not Found if no Practitioner with that id exists in your project, and 410 Gone if it was deleted.

Path parameters
  • ididRequired

    The Practitioner id.

GET/fhir/R4/Practitioner/{id}
curl "https://api.medplum.konko.ai/fhir/R4/Practitioner/{practitioner_id}" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
200 OK  Response
{
  "resourceType": "Practitioner",
  "id": "d1b5e8c3-4f7a-4d2e-9b6c-8a0f3e5d1c72",
  "meta": {
    "versionId": "9e2b5d8f-4c1a-4b7e-8d3f-2a6c0e9b5d17",
    "lastUpdated": "2026-09-20T13:44:08.771Z"
  },
  "identifier": [
    {
      "system": "https://konko.ai/huli/doctor",
      "value": "1000"
    },
    {
      "system": "https://konko.ai/huli/user",
      "value": "10000"
    },
    {
      "system": "https://konko.ai/fhir/knowledge-base-id",
      "value": "DOC001"
    }
  ],
  "active": true,
  "name": [
    {
      "use": "official",
      "text": "Dra. Carolina Jiménez Soto",
      "family": "Jiménez Soto",
      "given": [
        "Carolina"
      ]
    }
  ],
  "telecom": [
    {
      "system": "phone",
      "value": "88112233"
    },
    {
      "system": "email",
      "value": "dra.jimenez@clinicahorizonte.example"
    }
  ],
  "gender": "female",
  "qualification": [
    {
      "identifier": [
        {
          "system": "https://konko.ai/huli/professional-license",
          "value": "MED-12345"
        }
      ],
      "code": {
        "text": "Colegio de Médicos y Cirujanos"
      }
    },
    {
      "identifier": [
        {
          "system": "https://konko.ai/huli/specialty",
          "value": "41"
        }
      ],
      "code": {
        "text": "Ginecología y Obstetricia"
      }
    }
  ]
}
Providers

PractitionerRole

A practitioner working at a location. It links the doctor to the place and the services they offer, and it's what appointments point to.

  • Clinic configuration creates one role for every practitioner at every location where they work. Roles synced from an EHR or a calendar may lack organization or location.
  • To find everything about one doctor, start from their roles: GET /PractitionerRole?practitioner=Practitioner/{id}.
AttributesPopulated = filled by Konko
  • idid1..1

    Konko's ID for this resource, a UUID. It never changes.

  • metaMeta1..1

    Version information, maintained by the server.

    2 child attributes
    • versionIdid1..1

      Changes on every update. Use it to detect changes and to ignore duplicate webhook deliveries.

    • lastUpdatedinstant1..1

      When this version was saved, in UTC. Search on it with _lastUpdated.

  • identifierIdentifier0..*Populated

    IDs from the systems this record came from. Match on system and value:

    https://konko.ai/practitioner-role: practitioner code and location code, such as DOC001:LOC001.

    https://konko.ai/huli/practitioner-role: Huli doctor ID and clinic ID, as {doctorId}:{clinicId}.

    https://konko.ai/fhir/identifier/google-calendar-practitioner-role: email of the connected Google Calendar.

  • activeboolean0..1Populated

    Whether the role is in use.

  • practitionerReference(Practitioner)0..1Populated

    The practitioner.

  • organizationReference(Organization)0..1Populated

    The organization the practitioner works for in this role. Organizations aren't part of this API.

  • locationReference(Location)0..*Populated

    Where the practitioner works in this role. Normally one location.

  • healthcareServiceReference(HealthcareService)0..*Populated

    Services this practitioner is specifically assigned to. A service that any practitioner can provide isn't listed, so an empty list doesn't mean the practitioner offers nothing.

  • specialtyCodeableConcept0..*Populated

    Specialty as text only, for example {"text": "Ginecología"}.

  • telecomContactPoint0..*Populated

    Contact details for this role.

    3 child attributes
    • systemcode0..1

      phone, email or url.

    • valuestring0..1

      The number, address or URL, as stored by the source.

    • usecode0..1

      mobile on numbers collected by Konko's assistant.

  • extensionExtension0..*Populated

    Konko extensions.

    1 child attribute
    • timezonevalueCode0..1

      url: http://hl7.org/fhir/StructureDefinition/timezone

      IANA time zone, for example America/Costa_Rica.

  • languagecode0..1Populated

    es on resources that come from the clinic's configuration in Konko.

  • Other FHIR fields

    period, code, availableTime, notAvailable and availabilityExceptions are part of the standard resource. Konko doesn't fill them today, so treat them as optional.

The PractitionerRole object
{
  "resourceType": "PractitionerRole",
  "id": "f7c3a9e5-1b8d-4e6f-a2c4-9d7b0e3f5a18",
  "meta": {
    "versionId": "2c7e0a4d-9f3b-4d6a-b1e8-5f2c9a7d0e43",
    "lastUpdated": "2026-09-14T17:02:17.056Z"
  },
  "language": "es",
  "extension": [
    {
      "url": "http://hl7.org/fhir/StructureDefinition/timezone",
      "valueCode": "America/Costa_Rica"
    }
  ],
  "identifier": [
    {
      "system": "https://konko.ai/practitioner-role",
      "value": "DOC001:LOC001"
    }
  ],
  "active": true,
  "practitioner": {
    "reference": "Practitioner/d1b5e8c3-4f7a-4d2e-9b6c-8a0f3e5d1c72"
  },
  "organization": {
    "reference": "Organization/b2d7e4a1-5c3f-4e9b-8a1d-6f2c0e7b9d35"
  },
  "specialty": [
    {
      "text": "Ginecología"
    }
  ],
  "location": [
    {
      "reference": "Location/e4a9c1d7-2b6f-4a3e-9c8d-5f1b7e0a2c64"
    }
  ],
  "healthcareService": [
    {
      "reference": "HealthcareService/a3f6b8d2-9e1c-4b7a-8d5f-2c0e6a9b1d47"
    }
  ]
}

Retrieve a practitioner role

GET/fhir/R4/PractitionerRole/{id}

Returns a practitioner role by its id. The response carries ETag: W/"<versionId>" and Last-Modified headers.

Returns 404 Not Found if no PractitionerRole with that id exists in your project, and 410 Gone if it was deleted.

Path parameters
  • ididRequired

    The PractitionerRole id.

GET/fhir/R4/PractitionerRole/{id}
curl "https://api.medplum.konko.ai/fhir/R4/PractitionerRole/{practitionerrole_id}" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
200 OK  Response
{
  "resourceType": "PractitionerRole",
  "id": "f7c3a9e5-1b8d-4e6f-a2c4-9d7b0e3f5a18",
  "meta": {
    "versionId": "2c7e0a4d-9f3b-4d6a-b1e8-5f2c9a7d0e43",
    "lastUpdated": "2026-09-14T17:02:17.056Z"
  },
  "language": "es",
  "extension": [
    {
      "url": "http://hl7.org/fhir/StructureDefinition/timezone",
      "valueCode": "America/Costa_Rica"
    }
  ],
  "identifier": [
    {
      "system": "https://konko.ai/practitioner-role",
      "value": "DOC001:LOC001"
    }
  ],
  "active": true,
  "practitioner": {
    "reference": "Practitioner/d1b5e8c3-4f7a-4d2e-9b6c-8a0f3e5d1c72"
  },
  "organization": {
    "reference": "Organization/b2d7e4a1-5c3f-4e9b-8a1d-6f2c0e7b9d35"
  },
  "specialty": [
    {
      "text": "Ginecología"
    }
  ],
  "location": [
    {
      "reference": "Location/e4a9c1d7-2b6f-4a3e-9c8d-5f1b7e0a2c64"
    }
  ],
  "healthcareService": [
    {
      "reference": "HealthcareService/a3f6b8d2-9e1c-4b7a-8d5f-2c0e6a9b1d47"
    }
  ]
}
Providers

Location

A site where patients are seen: a clinic, an office, or a virtual location for remote consultations.

  • Opening hours are in hoursOfOperation, with a human-readable version in the operating-hours-narrative extension. All times are local to the location's time zone.
AttributesPopulated = filled by Konko
  • idid1..1

    Konko's ID for this resource, a UUID. It never changes.

  • metaMeta1..1

    Version information, maintained by the server.

    2 child attributes
    • versionIdid1..1

      Changes on every update. Use it to detect changes and to ignore duplicate webhook deliveries.

    • lastUpdatedinstant1..1

      When this version was saved, in UTC. Search on it with _lastUpdated.

  • identifierIdentifier0..*Populated

    IDs from the systems this record came from. Match on system and value:

    https://konko.ai/location: the location's code in the clinic configuration, such as LOC001.

    https://konko.ai/huli/clinic: clinic ID in the Huli EHR.

    https://konko.ai/fhir/knowledge-base-id: the LOC### code on locations Konko's assistant books for.

  • statuscode0..1Populated

    active, suspended or inactive. Set on locations synced from an EHR.

  • namestring0..1Populated

    The name patients know the site by.

  • descriptionstring0..1Populated

    Directions and other details for patients.

  • typeCodeableConcept0..*Populated

    On locations synced from an EHR: PHYSICAL or VIRTUAL in the system https://konko.ai/huli/clinic-type.

  • telecomContactPoint0..*Populated

    Phone and email for this site.

    3 child attributes
    • systemcode0..1

      phone, email or url.

    • valuestring0..1

      The number, address or URL, as stored by the source.

    • usecode0..1

      mobile on numbers collected by Konko's assistant.

  • addressAddress0..1Populated

    One address. Always has text; locations from an EHR also have city, state and country.

  • positionBackboneElement0..1Populated

    latitude and longitude, when the EHR provides them.

  • managingOrganizationReference(Organization)0..1Populated

    The organization that runs the site, either as {"reference": "Organization/{id}"} or as an identifier only. Organizations aren't part of this API.

  • hoursOfOperationBackboneElement0..*Populated

    Regular opening hours.

    3 child attributes
    • daysOfWeekcode0..*

      mon to sun.

    • openingTimetime0..1

      Local time, HH:MM:SS.

    • closingTimetime0..1

      Local time, HH:MM:SS.

  • extensionExtension0..*Populated

    Konko extensions.

    3 child attributes
    • timezonevalueCode0..1

      url: http://hl7.org/fhir/StructureDefinition/timezone

      IANA time zone, for example America/Costa_Rica.

    • operating-hours-narrativevalueString0..1

      url: https://konko.ai/location/operating-hours-narrative

      Opening hours as a sentence for patients.

    • location-urlvalueString0..1

      url: https://konko.ai/location/location-url

      A map link.

  • languagecode0..1Populated

    es on resources that come from the clinic's configuration in Konko.

  • Other FHIR fields

    mode, alias, operationalStatus, physicalType, partOf and availabilityExceptions are part of the standard resource. Konko doesn't fill them today, so treat them as optional.

The Location object
{
  "resourceType": "Location",
  "id": "e4a9c1d7-2b6f-4a3e-9c8d-5f1b7e0a2c64",
  "meta": {
    "versionId": "8d1f4b6e-2a7c-4e3d-9b5f-6c0e8a2d4f71",
    "lastUpdated": "2026-09-14T17:02:12.918Z"
  },
  "language": "es",
  "extension": [
    {
      "url": "http://hl7.org/fhir/StructureDefinition/timezone",
      "valueCode": "America/Costa_Rica"
    },
    {
      "url": "https://konko.ai/location/operating-hours-narrative",
      "valueString": "Lunes a viernes de 8:00 a 17:00; sábados de 8:00 a 12:00"
    },
    {
      "url": "https://konko.ai/location/location-url",
      "valueString": "https://maps.example.com/?q=clinica-horizonte-escazu"
    }
  ],
  "identifier": [
    {
      "system": "https://konko.ai/location",
      "value": "LOC001"
    }
  ],
  "name": "Sede Escazú",
  "description": "Torre B, piso 3. Parqueo gratuito en el sótano.",
  "telecom": [
    {
      "system": "phone",
      "value": "+506 2288 0000"
    }
  ],
  "address": {
    "text": "Plaza Médica Horizonte, Escazú, San José"
  },
  "managingOrganization": {
    "identifier": {
      "system": "https://konko.ai/clinic-details",
      "value": "clinica_horizonte"
    }
  },
  "hoursOfOperation": [
    {
      "daysOfWeek": [
        "mon",
        "tue",
        "wed",
        "thu",
        "fri"
      ],
      "openingTime": "08:00:00",
      "closingTime": "17:00:00"
    },
    {
      "daysOfWeek": [
        "sat"
      ],
      "openingTime": "08:00:00",
      "closingTime": "12:00:00"
    }
  ]
}

Retrieve a location

GET/fhir/R4/Location/{id}

Returns a location by its id. The response carries ETag: W/"<versionId>" and Last-Modified headers.

Returns 404 Not Found if no Location with that id exists in your project, and 410 Gone if it was deleted.

Path parameters
  • ididRequired

    The Location id.

GET/fhir/R4/Location/{id}
curl "https://api.medplum.konko.ai/fhir/R4/Location/{location_id}" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
200 OK  Response
{
  "resourceType": "Location",
  "id": "e4a9c1d7-2b6f-4a3e-9c8d-5f1b7e0a2c64",
  "meta": {
    "versionId": "8d1f4b6e-2a7c-4e3d-9b5f-6c0e8a2d4f71",
    "lastUpdated": "2026-09-14T17:02:12.918Z"
  },
  "language": "es",
  "extension": [
    {
      "url": "http://hl7.org/fhir/StructureDefinition/timezone",
      "valueCode": "America/Costa_Rica"
    },
    {
      "url": "https://konko.ai/location/operating-hours-narrative",
      "valueString": "Lunes a viernes de 8:00 a 17:00; sábados de 8:00 a 12:00"
    },
    {
      "url": "https://konko.ai/location/location-url",
      "valueString": "https://maps.example.com/?q=clinica-horizonte-escazu"
    }
  ],
  "identifier": [
    {
      "system": "https://konko.ai/location",
      "value": "LOC001"
    }
  ],
  "name": "Sede Escazú",
  "description": "Torre B, piso 3. Parqueo gratuito en el sótano.",
  "telecom": [
    {
      "system": "phone",
      "value": "+506 2288 0000"
    }
  ],
  "address": {
    "text": "Plaza Médica Horizonte, Escazú, San José"
  },
  "managingOrganization": {
    "identifier": {
      "system": "https://konko.ai/clinic-details",
      "value": "clinica_horizonte"
    }
  },
  "hoursOfOperation": [
    {
      "daysOfWeek": [
        "mon",
        "tue",
        "wed",
        "thu",
        "fri"
      ],
      "openingTime": "08:00:00",
      "closingTime": "17:00:00"
    },
    {
      "daysOfWeek": [
        "sat"
      ],
      "openingTime": "08:00:00",
      "closingTime": "12:00:00"
    }
  ]
}
Providers

HealthcareService

A service patients can book, such as a first consultation or a procedure, with its price range, preparation instructions and where it's offered.

  • Services come from your clinic's configuration in Konko. The appointment type in type (for example office-visit) is the code that links a service to its booked slots.
  • Prices are display text for patients (₡35 000), not amounts to calculate with. The currency is in its own extension.
AttributesPopulated = filled by Konko
  • idid1..1

    Konko's ID for this resource, a UUID. It never changes.

  • metaMeta1..1

    Version information, maintained by the server.

    2 child attributes
    • versionIdid1..1

      Changes on every update. Use it to detect changes and to ignore duplicate webhook deliveries.

    • lastUpdatedinstant1..1

      When this version was saved, in UTC. Search on it with _lastUpdated.

  • identifierIdentifier0..*Populated

    IDs from the systems this record came from. Match on system and value:

    https://konko.ai/healthcare-service: the service's code in the clinic configuration, such as SVC003.

  • activeboolean0..1Populated

    Whether patients can book the service.

  • namestring0..1Populated

    The name shown to patients.

  • providedByReference(Organization)0..1Populated

    The organization offering the service. Organizations aren't part of this API.

  • categoryCodeableConcept0..*Populated

    A grouping as text only, for example Consultas.

  • typeCodeableConcept0..*Populated

    The appointment type, as a code without a system, such as office-visit. The same code appears on booked slots, in Slot.serviceType.

  • specialtyCodeableConcept0..*Populated

    Sub-category, coded in https://konko.ai/service-sub-category/CodeSystem/{clinic}.

  • locationReference(Location)0..*Populated

    Where the service is offered.

  • commentstring0..1Populated

    A short description.

  • extraDetailsmarkdown0..1Populated

    Longer details for patients.

  • appointmentRequiredboolean0..1Populated

    Whether the service needs an appointment.

  • extensionExtension0..*Populated

    Konko extensions. The URLs all start with https://konko.ai/healthcare-service/.

    9 child attributes
    • price-minimum-narrative, price-maximum-narrativevalueString0..1

      Lowest and highest price, as display text.

    • currencyvalueString0..1

      ISO 4217 currency code, such as CRC.

    • service-prerequisitesvalueString0..1

      What the patient must do beforehand.

    • consent-instructions, additional-instructionsvalueString0..1

      Consent and other instructions for patients.

    • results-lead-timevalueString0..1

      How long results usually take, as text.

    • appointment-type-codevalueString0..1

      The appointment type code, the same as in type.

    • auto-scheduling-allowedvalueBoolean0..1

      Whether Konko's assistant can book the service without staff.

    • all-locationsvalueBoolean0..1

      Whether the service is offered at every location.

    • SchedulingParametersExtension0..1

      url: https://medplum.com/fhir/StructureDefinition/SchedulingParameters

      The service's appointment length in duration.

  • languagecode0..1Populated

    es on resources that come from the clinic's configuration in Konko.

The HealthcareService object
{
  "resourceType": "HealthcareService",
  "id": "a3f6b8d2-9e1c-4b7a-8d5f-2c0e6a9b1d47",
  "meta": {
    "versionId": "3a6c9e1f-7b2d-4f8a-a5c3-1e9d7b0f2a64",
    "lastUpdated": "2026-09-14T17:02:15.402Z"
  },
  "language": "es",
  "extension": [
    {
      "url": "https://konko.ai/healthcare-service/price-minimum-narrative",
      "valueString": "₡35 000"
    },
    {
      "url": "https://konko.ai/healthcare-service/price-maximum-narrative",
      "valueString": "₡45 000"
    },
    {
      "url": "https://konko.ai/healthcare-service/currency",
      "valueString": "CRC"
    },
    {
      "url": "https://konko.ai/healthcare-service/service-prerequisites",
      "valueString": "No requiere ayuno."
    },
    {
      "url": "https://konko.ai/healthcare-service/additional-instructions",
      "valueString": "Traer exámenes previos."
    },
    {
      "url": "https://konko.ai/healthcare-service/appointment-type-code",
      "valueString": "office-visit"
    },
    {
      "url": "https://konko.ai/healthcare-service/auto-scheduling-allowed",
      "valueBoolean": true
    },
    {
      "url": "https://konko.ai/healthcare-service/all-locations",
      "valueBoolean": false
    },
    {
      "url": "https://medplum.com/fhir/StructureDefinition/SchedulingParameters",
      "extension": [
        {
          "url": "duration",
          "valueDuration": {
            "value": 30,
            "unit": "min"
          }
        },
        {
          "url": "alignmentInterval",
          "valueDuration": {
            "value": 30,
            "unit": "min"
          }
        }
      ]
    }
  ],
  "identifier": [
    {
      "system": "https://konko.ai/healthcare-service",
      "value": "SVC003"
    }
  ],
  "active": true,
  "providedBy": {
    "reference": "Organization/b2d7e4a1-5c3f-4e9b-8a1d-6f2c0e7b9d35"
  },
  "category": [
    {
      "text": "Consultas"
    }
  ],
  "type": [
    {
      "coding": [
        {
          "code": "office-visit",
          "display": "Consulta"
        }
      ]
    }
  ],
  "specialty": [
    {
      "coding": [
        {
          "system": "https://konko.ai/service-sub-category/CodeSystem/clinica_horizonte",
          "code": "ginecologia-y-obstetricia",
          "display": "Ginecología y Obstetricia"
        }
      ]
    }
  ],
  "location": [
    {
      "reference": "Location/e4a9c1d7-2b6f-4a3e-9c8d-5f1b7e0a2c64"
    }
  ],
  "name": "Consulta de Ginecología",
  "comment": "Valoración ginecológica general.",
  "extraDetails": "Incluye revisión de exámenes previos.",
  "appointmentRequired": true
}

Retrieve a service

GET/fhir/R4/HealthcareService/{id}

Returns a service by its id. The response carries ETag: W/"<versionId>" and Last-Modified headers.

Returns 404 Not Found if no HealthcareService with that id exists in your project, and 410 Gone if it was deleted.

Path parameters
  • ididRequired

    The HealthcareService id.

GET/fhir/R4/HealthcareService/{id}
curl "https://api.medplum.konko.ai/fhir/R4/HealthcareService/{healthcareservice_id}" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
200 OK  Response
{
  "resourceType": "HealthcareService",
  "id": "a3f6b8d2-9e1c-4b7a-8d5f-2c0e6a9b1d47",
  "meta": {
    "versionId": "3a6c9e1f-7b2d-4f8a-a5c3-1e9d7b0f2a64",
    "lastUpdated": "2026-09-14T17:02:15.402Z"
  },
  "language": "es",
  "extension": [
    {
      "url": "https://konko.ai/healthcare-service/price-minimum-narrative",
      "valueString": "₡35 000"
    },
    {
      "url": "https://konko.ai/healthcare-service/price-maximum-narrative",
      "valueString": "₡45 000"
    },
    {
      "url": "https://konko.ai/healthcare-service/currency",
      "valueString": "CRC"
    },
    {
      "url": "https://konko.ai/healthcare-service/service-prerequisites",
      "valueString": "No requiere ayuno."
    },
    {
      "url": "https://konko.ai/healthcare-service/additional-instructions",
      "valueString": "Traer exámenes previos."
    },
    {
      "url": "https://konko.ai/healthcare-service/appointment-type-code",
      "valueString": "office-visit"
    },
    {
      "url": "https://konko.ai/healthcare-service/auto-scheduling-allowed",
      "valueBoolean": true
    },
    {
      "url": "https://konko.ai/healthcare-service/all-locations",
      "valueBoolean": false
    },
    {
      "url": "https://medplum.com/fhir/StructureDefinition/SchedulingParameters",
      "extension": [
        {
          "url": "duration",
          "valueDuration": {
            "value": 30,
            "unit": "min"
          }
        },
        {
          "url": "alignmentInterval",
          "valueDuration": {
            "value": 30,
            "unit": "min"
          }
        }
      ]
    }
  ],
  "identifier": [
    {
      "system": "https://konko.ai/healthcare-service",
      "value": "SVC003"
    }
  ],
  "active": true,
  "providedBy": {
    "reference": "Organization/b2d7e4a1-5c3f-4e9b-8a1d-6f2c0e7b9d35"
  },
  "category": [
    {
      "text": "Consultas"
    }
  ],
  "type": [
    {
      "coding": [
        {
          "code": "office-visit",
          "display": "Consulta"
        }
      ]
    }
  ],
  "specialty": [
    {
      "coding": [
        {
          "system": "https://konko.ai/service-sub-category/CodeSystem/clinica_horizonte",
          "code": "ginecologia-y-obstetricia",
          "display": "Ginecología y Obstetricia"
        }
      ]
    }
  ],
  "location": [
    {
      "reference": "Location/e4a9c1d7-2b6f-4a3e-9c8d-5f1b7e0a2c64"
    }
  ],
  "name": "Consulta de Ginecología",
  "comment": "Valoración ginecológica general.",
  "extraDetails": "Incluye revisión de exámenes previos.",
  "appointmentRequired": true
}
Scheduling

Slot

A block of busy time on a practitioner's calendar: a booked appointment, time the clinic has blocked, or a holiday.

  • Konko stores only busy time; there are no free slots.
  • Slot IDs are not stable. When an appointment is cancelled its slot is deleted, and when it's rescheduled the slot is deleted and a new one created. Use the Appointment as your record of a booking.
AttributesPopulated = filled by Konko
  • idid1..1

    Konko's ID for this resource, a UUID. It never changes.

  • metaMeta1..1

    Version information, maintained by the server.

    2 child attributes
    • versionIdid1..1

      Changes on every update. Use it to detect changes and to ignore duplicate webhook deliveries.

    • lastUpdatedinstant1..1

      When this version was saved, in UTC. Search on it with _lastUpdated.

  • identifierIdentifier0..*Populated

    IDs from the systems this record came from. Match on system and value:

    https://konko.ai/huli/slot: event ID in the Huli EHR.

    https://konko.ai/fhir/identifier/google-calendar-event: Google Calendar event ID.

    https://konko.ai/holidays: YYYY-MM-DD:{scheduleId} on holidays from the clinic configuration.

  • scheduleReference(Schedule)1..1RequiredPopulated

    The practitioner's calendar this time belongs to. Schedules aren't part of this API; use the reference to group one calendar's slots.

  • statuscode1..1RequiredPopulated

    busy for an appointment. busy-unavailable for time blocked by the clinic, holidays and calendar events without patients.

  • startinstant1..1RequiredPopulated

    Start time, with the clinic's UTC offset.

  • endinstant1..1RequiredPopulated

    End time, with the clinic's UTC offset.

  • serviceTypeCodeableConcept0..*Populated

    Appointment type, on slots booked by Konko's assistant.

  • commentstring0..1Populated

    Title of the calendar event, for time blocked in Google Calendar.

  • extensionExtension0..*Populated

    Konko extensions.

    1 child attribute
    • holiday namevalueString0..1

      url: https://konko.ai/holidays/name

      Name of the holiday, such as Navidad.

  • Other FHIR fields

    serviceCategory, specialty, appointmentType and overbooked are part of the standard resource. Konko doesn't fill them today, so treat them as optional.

The Slot object
{
  "resourceType": "Slot",
  "id": "5e8b1d4f-7a2c-4e9d-8b3f-1c6a0e9d7b26",
  "meta": {
    "versionId": "4b9d2f7a-6e1c-4c8b-9a3d-7e0f5b2c9a16",
    "lastUpdated": "2026-10-05T15:21:44.210Z"
  },
  "schedule": {
    "reference": "Schedule/9a4e2c7b-5d1f-4b8a-9e3c-6f0d2a8b4e51"
  },
  "status": "busy",
  "start": "2026-10-13T08:30:00-06:00",
  "end": "2026-10-13T09:00:00-06:00",
  "serviceType": [
    {
      "coding": [
        {
          "code": "office-visit"
        }
      ]
    }
  ]
}

Retrieve a slot

GET/fhir/R4/Slot/{id}

Returns a slot by its id. The response carries ETag: W/"<versionId>" and Last-Modified headers.

Returns 404 Not Found if no Slot with that id exists in your project, and 410 Gone if it was deleted.

Path parameters
  • ididRequired

    The Slot id.

GET/fhir/R4/Slot/{id}
curl "https://api.medplum.konko.ai/fhir/R4/Slot/{slot_id}" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
200 OK  Response
{
  "resourceType": "Slot",
  "id": "5e8b1d4f-7a2c-4e9d-8b3f-1c6a0e9d7b26",
  "meta": {
    "versionId": "4b9d2f7a-6e1c-4c8b-9a3d-7e0f5b2c9a16",
    "lastUpdated": "2026-10-05T15:21:44.210Z"
  },
  "schedule": {
    "reference": "Schedule/9a4e2c7b-5d1f-4b8a-9e3c-6f0d2a8b4e51"
  },
  "status": "busy",
  "start": "2026-10-13T08:30:00-06:00",
  "end": "2026-10-13T09:00:00-06:00",
  "serviceType": [
    {
      "coding": [
        {
          "code": "office-visit"
        }
      ]
    }
  ]
}
Scheduling

Appointment

A booked visit: who it's for, with which practitioner, and when. Appointments come from your EHR, from connected calendars and from bookings made through Konko's assistant.

  • Participants are the patient and the practitioner's PractitionerRole, never the Practitioner itself. Follow the role to reach the practitioner and the location; to filter by practitioner, see the chained search below.
  • Cancelled appointments stay in place with status: cancelled, and their slot is deleted. A rescheduled appointment is normally updated in place with a new start and end.
  • The service booked is recorded on the slot (serviceType) and in description, not in Appointment.serviceType.
AttributesPopulated = filled by Konko
  • idid1..1

    Konko's ID for this resource, a UUID. It never changes.

  • metaMeta1..1

    Version information, maintained by the server.

    2 child attributes
    • versionIdid1..1

      Changes on every update. Use it to detect changes and to ignore duplicate webhook deliveries.

    • lastUpdatedinstant1..1

      When this version was saved, in UTC. Search on it with _lastUpdated.

  • identifierIdentifier0..*Populated

    IDs from the systems this record came from. Match on system and value:

    https://konko.ai/huli/appointment: appointment ID in the Huli EHR.

    https://konko.ai/fhir/identifier/google-calendar-event: Google Calendar event ID.

    Bookings made by Konko's assistant have no identifier until they're pushed to your EHR.

  • statuscode1..1RequiredPopulated

    booked, pending (a tentative calendar event), fulfilled, cancelled or noshow.

  • startinstant0..1Populated

    Start time, with the clinic's UTC offset.

  • endinstant0..1Populated

    End time, with the clinic's UTC offset.

  • participantBackboneElement1..*RequiredPopulated

    Who takes part. Time blocks from a calendar can have no patient.

    2 child attributes
    • actorReference(Patient | PractitionerRole)0..1

      The patient, or the practitioner's role.

    • statuscode1..1

      accepted, declined, tentative or needs-action.

  • slotReference(Slot)0..*Populated

    The busy slot the appointment occupies. When an appointment is cancelled, its slot is deleted.

  • descriptionstring0..1Populated

    Subject line, usually the service and the patient's name.

  • commentstring0..1Populated

    Free-text notes. On bookings made by Konko's assistant, the answers the patient gave while booking, one per line. Treat it as personal data.

  • Other FHIR fields

    serviceType, appointmentType, specialty, reasonCode, cancelationReason, minutesDuration and patientInstruction are part of the standard resource. Konko doesn't fill them today, so treat them as optional.

The Appointment object
{
  "resourceType": "Appointment",
  "id": "6b1f8e3d-2a9c-4b5e-8f7d-3e0a6c1b9d84",
  "meta": {
    "versionId": "a8f1c4e7-2b9d-4e6a-9c3f-5d0b8e2a7c14",
    "lastUpdated": "2026-10-05T15:21:47.382Z"
  },
  "identifier": [
    {
      "system": "https://konko.ai/huli/appointment",
      "value": "100"
    }
  ],
  "status": "booked",
  "description": "Consulta de Ginecología — Ana Lucía Mora Vargas",
  "start": "2026-10-13T08:30:00-06:00",
  "end": "2026-10-13T09:00:00-06:00",
  "slot": [
    {
      "reference": "Slot/5e8b1d4f-7a2c-4e9d-8b3f-1c6a0e9d7b26"
    }
  ],
  "comment": "full_name: Ana Lucía Mora Vargas\nphone_number: +50687654321\nservice: Consulta de Ginecología\nlocation: Sede Escazú",
  "participant": [
    {
      "actor": {
        "reference": "PractitionerRole/f7c3a9e5-1b8d-4e6f-a2c4-9d7b0e3f5a18"
      },
      "status": "tentative"
    },
    {
      "actor": {
        "reference": "Patient/3c9f6a1e-8d4b-4f2a-b5e7-0d9c2f8a6b13"
      },
      "status": "accepted"
    }
  ]
}

Retrieve an appointment

GET/fhir/R4/Appointment/{id}

Returns an appointment by its id. The response carries ETag: W/"<versionId>" and Last-Modified headers.

Returns 404 Not Found if no Appointment with that id exists in your project, and 410 Gone if it was deleted.

Path parameters
  • ididRequired

    The Appointment id.

GET/fhir/R4/Appointment/{id}
curl "https://api.medplum.konko.ai/fhir/R4/Appointment/{appointment_id}" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
200 OK  Response
{
  "resourceType": "Appointment",
  "id": "6b1f8e3d-2a9c-4b5e-8f7d-3e0a6c1b9d84",
  "meta": {
    "versionId": "a8f1c4e7-2b9d-4e6a-9c3f-5d0b8e2a7c14",
    "lastUpdated": "2026-10-05T15:21:47.382Z"
  },
  "identifier": [
    {
      "system": "https://konko.ai/huli/appointment",
      "value": "100"
    }
  ],
  "status": "booked",
  "description": "Consulta de Ginecología — Ana Lucía Mora Vargas",
  "start": "2026-10-13T08:30:00-06:00",
  "end": "2026-10-13T09:00:00-06:00",
  "slot": [
    {
      "reference": "Slot/5e8b1d4f-7a2c-4e9d-8b3f-1c6a0e9d7b26"
    }
  ],
  "comment": "full_name: Ana Lucía Mora Vargas\nphone_number: +50687654321\nservice: Consulta de Ginecología\nlocation: Sede Escazú",
  "participant": [
    {
      "actor": {
        "reference": "PractitionerRole/f7c3a9e5-1b8d-4e6f-a2c4-9d7b0e3f5a18"
      },
      "status": "tentative"
    },
    {
      "actor": {
        "reference": "Patient/3c9f6a1e-8d4b-4f2a-b5e7-0d9c2f8a6b13"
      },
      "status": "accepted"
    }
  ]
}
Webhooks

How webhooks work

Instead of polling, tell Konko which changes you care about and where to send them. When a matching resource is created, updated or deleted in your project, Konko sends it to your endpoint in an HTTPS POST.

A webhook is a FHIR R4 Subscription resource that you create and manage through the API. It holds three things: the changes you want (criteria), the URL to deliver them to (channel.endpoint), and delivery options such as a signing secret.

Change in your project create · update · delete Match subscriptions status=active, criteria Delivery queue one job per match Your endpoint HTTPS POST, 120 s timeout Done no retry signed 2xx/3xx other status, timeout or error retry after 1 s, 2 s, 4 s, 8 s …
Every attempt is logged. A failed attempt goes back to the queue and is retried with exponential backoff until it succeeds or the retry limit is reached.

What to expect

  • Triggers. Creating, updating or deleting a matching resource triggers a delivery. An update that changes nothing creates no new version and sends nothing.
  • New appointments can arrive as updates. Appointments booked through Konko's assistant are delivered with X-Medplum-Interaction: update, not create. Treat an appointment id you haven't seen before as new, whatever the interaction says.
  • Payload. Creates and updates carry the full resource as it is after the change. Deletes carry an empty JSON object and name the deleted resource in a header. See Receiving deliveries.
  • At least once. You can receive the same change more than once. Use the resource's meta.versionId to ignore versions you have already processed.
  • No ordering guarantee. Deliveries can arrive out of order. Compare meta.lastUpdated before overwriting newer data with older data.
  • Latest state wins on retries. If a resource changes again before a failed delivery is retried, the stale retry is dropped and you receive the newer version instead.
  • Fast responses. Reply with 2xx as soon as you have stored the event, then process it in the background. Deliveries time out after 120 seconds.
Important

Deliveries are not a substitute for a periodic catch-up. If your endpoint is down for longer than the retry window, events are dropped. After an outage, search with _lastUpdated=gt{time of your last delivery} to pick up the changes you missed.

Webhooks

The Subscription object

A standard FHIR R4 Subscription with the rest-hook channel, plus optional delivery settings carried as extensions.

Attributes
  • idid0..1Read-only

    Assigned by the server when you create the subscription.

  • statuscode1..1Required

    active to receive deliveries. Set it to off to pause a subscription without deleting it.

    Konko never changes this value for you. A subscription whose endpoint keeps failing stays active.

    Example: active
  • reasonstring1..1Required

    What the subscription is for, so you and Konko can tell subscriptions apart when debugging deliveries.

  • criteriastring1..1Required

    One resource type, optionally followed by search filters. See Choosing what to receive.

    Example: Appointment?status=booked,cancelled
  • channelBackboneElement1..1Required

    Where and how deliveries are sent.

    4 child attributes
    • typecode1..1Required

      Always rest-hook.

    • endpointurl0..1Required

      The HTTPS URL that receives deliveries. A subscription without an endpoint is accepted but never delivers.

    • payloadcode0..1

      Set to application/fhir+json. Deliveries always carry the full resource, whatever this field says.

    • headerstring0..*

      Extra HTTP headers to send with every delivery, each written as Name: value, for example an API key your endpoint expects.

      The value can't contain a colon (:). Anything after a second colon is dropped.

      Example: Authorization: Bearer 3f9c1a7e5b2d4c8f
  • extensionExtension0..*

    Delivery options. Each option is one entry with a url and a value.

    5 child attributes
    • subscription-secretvalueString0..1

      url: https://www.medplum.com/fhir/StructureDefinition/subscription-secret

      Signing secret. When set, every delivery carries an X-Signature header. See Verifying signatures. Use at least 32 random characters.

    • subscription-supported-interactionvalueCode0..1

      url: https://medplum.com/fhir/StructureDefinition/subscription-supported-interaction

      Deliver only one kind of change: create, update or delete. Without it you receive all three. To receive two of the three, create two subscriptions.

    • fhir-path-criteria-expressionvalueString0..1

      url: https://medplum.com/fhir/StructureDefinition/fhir-path-criteria-expression

      A FHIRPath expression that must evaluate to true for the change to be delivered. %previous is the version before the change and %current the version after it. For a newly created resource, %previous is empty.

      Example: %previous.status != 'cancelled' and %current.status = 'cancelled'
    • subscription-max-attemptsvalueInteger0..1

      url: https://medplum.com/fhir/StructureDefinition/subscription-max-attempts

      How many times to retry a failed delivery, from 1 to 18. The default is 4. See Retries.

    • subscription-success-codesvalueString0..1

      url: https://medplum.com/fhir/StructureDefinition/subscription-success-codes

      HTTP status codes that count as delivered, as a comma-separated list of codes and ranges. The default is 200-399.

      Example: 200-299,409
  • metaMeta0..1Read-only

    versionId and lastUpdated, maintained by the server.

The Subscription object
{
  "resourceType": "Subscription",
  "id": "2e7a4c9f-6b3d-4a1e-9c5b-8d0f7e2a4c39",
  "meta": {
    "versionId": "a1c4e7b2-9d3f-4e6a-8b5c-2f0d7e9a1b36",
    "lastUpdated": "2026-10-01T15:04:11.482Z"
  },
  "status": "active",
  "reason": "Sync appointment changes into our CRM",
  "criteria": "Appointment",
  "channel": {
    "type": "rest-hook",
    "endpoint": "https://integraciones.clinicahorizonte.example/konko/webhooks",
    "payload": "application/fhir+json",
    "header": [
      "Authorization: Bearer 3f9c1a7e5b2d4c8f"
    ]
  },
  "extension": [
    {
      "url": "https://www.medplum.com/fhir/StructureDefinition/subscription-secret",
      "valueString": "whsec_7b1d4e9a2c6f8b3e5a0d9c7f1e4b6a28"
    },
    {
      "url": "https://medplum.com/fhir/StructureDefinition/subscription-max-attempts",
      "valueInteger": 12
    }
  ]
}
Webhooks

Choosing what to receive

criteria names one resource type and, optionally, search filters that the changed resource must match. Two extensions narrow it further.

You wantcriteriaAlso set
Every appointment changeAppointment—
Booked and cancelled appointmentsAppointment?status=booked,cancelled—
Appointments of one practitioner at one locationAppointment?actor=PractitionerRole/{id}—
The moment an appointment is cancelledAppointmentFHIRPath %previous.status != 'cancelled' and %current.status = 'cancelled'
Time blocked by the clinicSlot?status=busy-unavailable—
Changes to patient recordsPatientinteraction update
Directory changes (one subscription each)Practitioner, PractitionerRole, HealthcareService, Location—

Filter rules

  • Use one resource type per subscription, from the types your client can read (see Access and permissions).
  • Filters are checked against the changed resource itself, using the same search parameters as the search endpoints. Token, string, reference, URI and date parameters work. Number and quantity parameters, chained parameters (such as patient.name), _include and _has don't.
  • Separate alternatives with commas (status=booked,cancelled). Repeat a parameter to require both conditions (date=ge2026-10-01&date=lt2026-11-01).
  • Date filters accept the usual prefixes (ge, gt, le, lt, eq, ne). :missing and :not work as in searches.
  • String filters match case-insensitively anywhere in the value, so name=ana also matches “Juliana”.
  • Konko doesn't validate criteria when you create the subscription. A typo simply never matches. Run the same query as a search first (GET /fhir/R4/Appointment?status=booked) to check that it returns what you expect.
Note

A filter such as Appointment?status=cancelled fires on every later edit of a cancelled appointment too. When you only want the moment something changes, use the FHIRPath extension with %previous and %current.

Webhooks

Manage subscriptions

Create, list, change and delete your webhooks. These are the only write operations your API client can perform.

Create a subscription

POST/fhir/R4/Subscription

Creates a webhook. Deliveries start with the next matching change; existing resources are not sent retroactively.

Send a Subscription object without id. Konko assigns the id and returns the stored subscription with 201 Created and a Location header.

Security

Keep the signing secret out of source control. Anyone who can read the Subscription through the API can see its secret and headers, so treat your API credentials as equally sensitive.

POST/fhir/R4/Subscription
curl -X POST "https://api.medplum.konko.ai/fhir/R4/Subscription" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/fhir+json" \
  -d '{
  "resourceType": "Subscription",
  "status": "active",
  "reason": "Sync appointment changes into our CRM",
  "criteria": "Appointment",
  "channel": {
    "type": "rest-hook",
    "endpoint": "https://integraciones.clinicahorizonte.example/konko/webhooks",
    "payload": "application/fhir+json",
    "header": [
      "Authorization: Bearer 3f9c1a7e5b2d4c8f"
    ]
  },
  "extension": [
    {
      "url": "https://www.medplum.com/fhir/StructureDefinition/subscription-secret",
      "valueString": "whsec_7b1d4e9a2c6f8b3e5a0d9c7f1e4b6a28"
    },
    {
      "url": "https://medplum.com/fhir/StructureDefinition/subscription-max-attempts",
      "valueInteger": 12
    }
  ]
}'
201 Created  Response
{
  "resourceType": "Subscription",
  "id": "2e7a4c9f-6b3d-4a1e-9c5b-8d0f7e2a4c39",
  "meta": {
    "versionId": "a1c4e7b2-9d3f-4e6a-8b5c-2f0d7e9a1b36",
    "lastUpdated": "2026-10-01T15:04:11.482Z"
  },
  "status": "active",
  "reason": "Sync appointment changes into our CRM",
  "criteria": "Appointment",
  "channel": {
    "type": "rest-hook",
    "endpoint": "https://integraciones.clinicahorizonte.example/konko/webhooks",
    "payload": "application/fhir+json",
    "header": [
      "Authorization: Bearer 3f9c1a7e5b2d4c8f"
    ]
  },
  "extension": [
    {
      "url": "https://www.medplum.com/fhir/StructureDefinition/subscription-secret",
      "valueString": "whsec_7b1d4e9a2c6f8b3e5a0d9c7f1e4b6a28"
    },
    {
      "url": "https://medplum.com/fhir/StructureDefinition/subscription-max-attempts",
      "valueInteger": 12
    }
  ]
}

List subscriptions

GET/fhir/R4/Subscription

Returns the subscriptions in your project as a searchset Bundle.

Query parameters
  • statustoken

    requested, active, error or off.

    Example: active
  • typetoken

    Channel type. Always rest-hook for webhooks.

  • urluri

    Exact endpoint URL.

    Example: https://integraciones.clinicahorizonte.example/konko/webhooks
  • criteriastring

    Text contained in criteria.

    Example: Appointment
  • _countinteger

    Page size, 1–1000. Default 20.

GET/fhir/R4/Subscription
curl -G "https://api.medplum.konko.ai/fhir/R4/Subscription" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  --data-urlencode "status=active"
200 OK  Response
{
  "resourceType": "Bundle",
  "type": "searchset",
  "entry": [
    {
      "fullUrl": "https://api.medplum.konko.ai/fhir/R4/Subscription/2e7a4c9f-6b3d-4a1e-9c5b-8d0f7e2a4c39",
      "resource": {
        "resourceType": "Subscription",
        "id": "2e7a4c9f-6b3d-4a1e-9c5b-8d0f7e2a4c39",
        "meta": {
          "versionId": "a1c4e7b2-9d3f-4e6a-8b5c-2f0d7e9a1b36",
          "lastUpdated": "2026-10-01T15:04:11.482Z"
        },
        "status": "active",
        "reason": "Sync appointment changes into our CRM",
        "criteria": "Appointment",
        "channel": {
          "type": "rest-hook",
          "endpoint": "https://integraciones.clinicahorizonte.example/konko/webhooks",
          "payload": "application/fhir+json",
          "header": [
            "Authorization: Bearer 3f9c1a7e5b2d4c8f"
          ]
        },
        "extension": [
          {
            "url": "https://www.medplum.com/fhir/StructureDefinition/subscription-secret",
            "valueString": "whsec_7b1d4e9a2c6f8b3e5a0d9c7f1e4b6a28"
          },
          {
            "url": "https://medplum.com/fhir/StructureDefinition/subscription-max-attempts",
            "valueInteger": 12
          }
        ]
      },
      "search": {
        "mode": "match"
      }
    }
  ],
  "link": [
    {
      "relation": "self",
      "url": "https://api.medplum.konko.ai/fhir/R4/Subscription?status=active"
    }
  ]
}

Retrieve a subscription

GET/fhir/R4/Subscription/{id}

Returns one subscription by id.

Path parameters
  • ididRequired

    The subscription id.

GET/fhir/R4/Subscription/{id}
curl "https://api.medplum.konko.ai/fhir/R4/Subscription/{subscription_id}" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
200 OK  Response
{
  "resourceType": "Subscription",
  "id": "2e7a4c9f-6b3d-4a1e-9c5b-8d0f7e2a4c39",
  "meta": {
    "versionId": "a1c4e7b2-9d3f-4e6a-8b5c-2f0d7e9a1b36",
    "lastUpdated": "2026-10-01T15:04:11.482Z"
  },
  "status": "active",
  "reason": "Sync appointment changes into our CRM",
  "criteria": "Appointment",
  "channel": {
    "type": "rest-hook",
    "endpoint": "https://integraciones.clinicahorizonte.example/konko/webhooks",
    "payload": "application/fhir+json",
    "header": [
      "Authorization: Bearer 3f9c1a7e5b2d4c8f"
    ]
  },
  "extension": [
    {
      "url": "https://www.medplum.com/fhir/StructureDefinition/subscription-secret",
      "valueString": "whsec_7b1d4e9a2c6f8b3e5a0d9c7f1e4b6a28"
    },
    {
      "url": "https://medplum.com/fhir/StructureDefinition/subscription-max-attempts",
      "valueInteger": 12
    }
  ]
}

Update a subscription

PUT/fhir/R4/Subscription/{id}

Replaces a subscription. Send the complete resource, including its id. Anything you leave out is removed.

Use it to pause deliveries (status: off), change the endpoint or change the signing secret. To change a secret without dropping deliveries, accept both the old and the new signature on your side for a few minutes while the update takes effect.

Path parameters
  • ididRequired

    The subscription id. Must match the id in the body.

PUT/fhir/R4/Subscription/{id}
curl -X PUT "https://api.medplum.konko.ai/fhir/R4/Subscription/{subscription_id}" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/fhir+json" \
  -d '{
  "resourceType": "Subscription",
  "id": "2e7a4c9f-6b3d-4a1e-9c5b-8d0f7e2a4c39",
  "status": "off",
  "reason": "Sync appointment changes into our CRM",
  "criteria": "Appointment",
  "channel": {
    "type": "rest-hook",
    "endpoint": "https://integraciones.clinicahorizonte.example/konko/webhooks",
    "payload": "application/fhir+json",
    "header": [
      "Authorization: Bearer 3f9c1a7e5b2d4c8f"
    ]
  },
  "extension": [
    {
      "url": "https://www.medplum.com/fhir/StructureDefinition/subscription-secret",
      "valueString": "whsec_7b1d4e9a2c6f8b3e5a0d9c7f1e4b6a28"
    },
    {
      "url": "https://medplum.com/fhir/StructureDefinition/subscription-max-attempts",
      "valueInteger": 12
    }
  ]
}'
200 OK  Response
{
  "resourceType": "Subscription",
  "id": "2e7a4c9f-6b3d-4a1e-9c5b-8d0f7e2a4c39",
  "status": "off",
  "reason": "Sync appointment changes into our CRM",
  "criteria": "Appointment",
  "channel": {
    "type": "rest-hook",
    "endpoint": "https://integraciones.clinicahorizonte.example/konko/webhooks",
    "payload": "application/fhir+json",
    "header": [
      "Authorization: Bearer 3f9c1a7e5b2d4c8f"
    ]
  },
  "extension": [
    {
      "url": "https://www.medplum.com/fhir/StructureDefinition/subscription-secret",
      "valueString": "whsec_7b1d4e9a2c6f8b3e5a0d9c7f1e4b6a28"
    },
    {
      "url": "https://medplum.com/fhir/StructureDefinition/subscription-max-attempts",
      "valueInteger": 12
    }
  ],
  "meta": {
    "versionId": "c7e1b9d4-2a6f-4c8e-9b3d-5f0a7e2c4b18",
    "lastUpdated": "2026-10-03T09:12:40.905Z"
  }
}

Delete a subscription

DELETE/fhir/R4/Subscription/{id}

Deletes a subscription. Deliveries that are already queued for it are cancelled before their next attempt.

Path parameters
  • ididRequired

    The subscription id.

DELETE/fhir/R4/Subscription/{id}
curl -X DELETE "https://api.medplum.konko.ai/fhir/R4/Subscription/{subscription_id}" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
200 OK  Response
{
  "resourceType": "OperationOutcome",
  "id": "ok",
  "issue": [
    {
      "severity": "information",
      "code": "informational",
      "details": {
        "text": "All OK"
      }
    }
  ]
}
Webhooks

Receiving deliveries

Each delivery is one HTTPS POST to your endpoint, carrying one changed resource.

Request headers
HeaderMeaning
Content-TypeAlways application/fhir+json.
X-Medplum-SubscriptionThe id of the subscription that matched.
X-Medplum-Interactioncreate, update or delete.
X-Medplum-Deleted-ResourceDeletes only. The deleted resource as Type/id, for example Appointment/6b1f….
X-SignaturePresent when the subscription has a signing secret. Hex-encoded HMAC-SHA256 of the raw body.
x-trace-id, traceparentPresent when the change carried a trace ID. Quote it when you report a problem to Konko.
Your headersEvery channel.header entry, as configured.

The body is the resource exactly as a read would return it at that version, so you can store it directly or pass it to the same code that handles API responses.

For a delete the body is {}. Take the resource type and id from X-Medplum-Deleted-Resource.

Bodies are sent as compact JSON without whitespace. The examples on this page are formatted for reading; their signatures are computed over the compact form.

Deliveries don't include related resources. If you need the patient of an appointment, read it or use _include on a search.

POSTUpdate delivery
POST /konko/webhooks HTTP/1.1
Host: integraciones.clinicahorizonte.example
Content-Type: application/fhir+json
X-Medplum-Subscription: 2e7a4c9f-6b3d-4a1e-9c5b-8d0f7e2a4c39
X-Medplum-Interaction: update
X-Signature: bbc14666f59330c5ceaca8b582042dd28bdc05460504b692313b0002444d079c
Authorization: Bearer 3f9c1a7e5b2d4c8f

{
  "resourceType": "Appointment",
  "id": "6b1f8e3d-2a9c-4b5e-8f7d-3e0a6c1b9d84",
  "meta": {
    "versionId": "0d6b3f9e-4a1c-4e7b-8f2d-9c5a1e7b3d60",
    "lastUpdated": "2026-10-09T16:20:02.841Z"
  },
  "identifier": [
    {
      "system": "https://konko.ai/huli/appointment",
      "value": "100"
    }
  ],
  "status": "cancelled",
  "description": "Consulta de Ginecología — Ana Lucía Mora Vargas",
  "start": "2026-10-13T08:30:00-06:00",
  "end": "2026-10-13T09:00:00-06:00",
  "slot": [
    {
      "reference": "Slot/5e8b1d4f-7a2c-4e9d-8b3f-1c6a0e9d7b26"
    }
  ],
  "comment": "full_name: Ana Lucía Mora Vargas\nphone_number: +50687654321\nservice: Consulta de Ginecología\nlocation: Sede Escazú",
  "participant": [
    {
      "actor": {
        "reference": "PractitionerRole/f7c3a9e5-1b8d-4e6f-a2c4-9d7b0e3f5a18"
      },
      "status": "tentative"
    },
    {
      "actor": {
        "reference": "Patient/3c9f6a1e-8d4b-4f2a-b5e7-0d9c2f8a6b13"
      },
      "status": "accepted"
    }
  ]
}
POSTDelete delivery
POST /konko/webhooks HTTP/1.1
Host: integraciones.clinicahorizonte.example
Content-Type: application/fhir+json
X-Medplum-Subscription: 2e7a4c9f-6b3d-4a1e-9c5b-8d0f7e2a4c39
X-Medplum-Interaction: delete
X-Medplum-Deleted-Resource: Appointment/6b1f8e3d-2a9c-4b5e-8f7d-3e0a6c1b9d84
X-Signature: e77eb15d139ff89a6849bd1f20f78a3583aaa46ead148ba2779d94eb75371470
Authorization: Bearer 3f9c1a7e5b2d4c8f

{}
Webhooks

Verifying signatures

Set a signing secret on each subscription and reject any delivery whose signature doesn't match.

  • Konko computes an HMAC-SHA256 of the request body with your secret and sends the hex digest in X-Signature.
  • Compute the same digest over the raw body bytes before parsing the JSON. Re-serialized JSON won't match.
  • Compare with a constant-time function such as hmac.compare_digest or crypto.timingSafeEqual.
  • Delete deliveries are signed too. Their body is {}.
  • The signature proves the body came from Konko and wasn't modified. It doesn't include a timestamp, so rely on meta.versionId to drop replays and duplicates.
Note

Serve your endpoint over HTTPS only. Custom channel.header values, such as an API key, give you a second check that the request came from Konko.

Verify X-Signature
import hashlib
import hmac

WEBHOOK_SECRET = "whsec_7b1d4e9a2c6f8b3e5a0d9c7f1e4b6a28"


def is_valid_signature(raw_body: bytes, signature: str | None) -> bool:
    """raw_body must be the exact bytes received, before any JSON parsing."""
    if not signature:
        return False
    expected = hmac.new(WEBHOOK_SECRET.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)


# FastAPI example
from fastapi import FastAPI, HTTPException, Request

app = FastAPI()


@app.post("/konko/webhooks")
async def konko_webhook(request: Request):
    raw = await request.body()
    if not is_valid_signature(raw, request.headers.get("X-Signature")):
        raise HTTPException(status_code=401)
    enqueue_for_processing(
        interaction=request.headers["X-Medplum-Interaction"],
        deleted=request.headers.get("X-Medplum-Deleted-Resource"),
        body=raw,
    )
    return {"received": True}
Webhooks

Retries

A delivery succeeds when your endpoint answers with a status from 200 to 399. Anything else is retried with exponential backoff.

  • Failures are any other status code, no response within 120 seconds, or a connection or TLS error. Redirects are followed.
  • Retries wait 1 second, then 2, 4, 8 and so on, doubling each time.
  • The default is 4 retries. Raise it with the subscription-max-attempts extension (up to 18) if your endpoint can be unavailable for longer. To change which status codes count as success, use subscription-success-codes.
  • After the last retry the delivery is dropped. The subscription stays active and keeps receiving new changes.
  • While a delivery is waiting to be retried, the resource may change again. The stale retry is then skipped and the newer version is delivered.
subscription-max-attemptsTotal attemptsLast attempt, after the first failure
4 (default)5about 15 seconds
89about 4 minutes
1213about 1 hour 8 minutes
1516about 9 hours
18 (maximum)19about 73 hours

Times are approximate and exclude the time your endpoint takes to respond.

Webhooks

Delivery log

Check what Konko sent to your endpoint and why a delivery failed.

List delivery attempts

GET/fhir/R4/AuditEvent

Every delivery attempt is recorded as an AuditEvent of type transmit. Search them by subscription to see what was sent, when, and what your endpoint answered.

Query parameters
  • entityreferenceRequired

    Subscription/{id} for one webhook, or the changed resource (Appointment/{id}) to see every delivery about it.

    Example: Subscription/2e7a4c9f-6b3d-4a1e-9c5b-8d0f7e2a4c39
  • outcometoken

    0 for successful attempts, 4 for failed ones.

  • datedate

    When the attempt was recorded. Supports ge, le and the other date prefixes.

    Example: ge2026-10-02
  • _sortstring

    -date for newest first.

What each attempt records
  • outcomecode

    0 delivered, 4 failed.

  • outcomeDescstring

    The attempt number (starting at 0) and your endpoint's status code, or the network error.

    Example: Attempt 0 received status 503
  • periodPeriod

    start is when the change happened, end is when the attempt finished.

  • entityBackboneElement[]

    The changed resource (role 4, or 1 for a Patient) and the subscription (role 9).

GET/fhir/R4/AuditEvent
curl -G "https://api.medplum.konko.ai/fhir/R4/AuditEvent" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  --data-urlencode "entity=Subscription/{subscription_id}" \
  --data-urlencode "_sort=-date" \
  --data-urlencode "_count=20"
200 OK  Response
{
  "resourceType": "Bundle",
  "type": "searchset",
  "entry": [
    {
      "fullUrl": "https://api.medplum.konko.ai/fhir/R4/AuditEvent/4d8c2f6a-9e5b-4c1d-a3f7-6b0e8d4c2a95",
      "resource": {
        "resourceType": "AuditEvent",
        "id": "4d8c2f6a-9e5b-4c1d-a3f7-6b0e8d4c2a95",
        "meta": {
          "versionId": "e2b7d4a9-6c1f-4e3b-a8d5-0f9c2e7b4a61",
          "lastUpdated": "2026-10-02T16:20:03.117Z"
        },
        "period": {
          "start": "2026-10-02T16:20:02.841Z",
          "end": "2026-10-02T16:20:03.109Z"
        },
        "recorded": "2026-10-02T16:20:03.109Z",
        "type": {
          "code": "transmit"
        },
        "agent": [
          {
            "type": {
              "text": "Subscription"
            },
            "requestor": false
          }
        ],
        "source": {
          "observer": {
            "reference": "Subscription/2e7a4c9f-6b3d-4a1e-9c5b-8d0f7e2a4c39"
          }
        },
        "entity": [
          {
            "what": {
              "reference": "Appointment/6b1f8e3d-2a9c-4b5e-8f7d-3e0a6c1b9d84"
            },
            "role": {
              "code": "4",
              "display": "Domain"
            }
          },
          {
            "what": {
              "reference": "Subscription/2e7a4c9f-6b3d-4a1e-9c5b-8d0f7e2a4c39"
            },
            "role": {
              "code": "9",
              "display": "Subscriber"
            }
          }
        ],
        "outcome": "4",
        "outcomeDesc": "Attempt 0 received status 503"
      },
      "search": {
        "mode": "match"
      }
    }
  ],
  "link": [
    {
      "relation": "self",
      "url": "https://api.medplum.konko.ai/fhir/R4/AuditEvent?entity=Subscription/2e7a4c9f-6b3d-4a1e-9c5b-8d0f7e2a4c39&_sort=-date&_count=20"
    },
    {
      "relation": "first",
      "url": "https://api.medplum.konko.ai/fhir/R4/AuditEvent?entity=Subscription/2e7a4c9f-6b3d-4a1e-9c5b-8d0f7e2a4c39&_sort=-date&_count=20&_offset=0"
    },
    {
      "relation": "next",
      "url": "https://api.medplum.konko.ai/fhir/R4/AuditEvent?entity=Subscription/2e7a4c9f-6b3d-4a1e-9c5b-8d0f7e2a4c39&_sort=-date&_count=20&_offset=20"
    }
  ]
}