Documentation

Everything you need to get up and running with Genkō.

Already have an account? Sign in →

On this page

Getting Started

Dashboard overview

After signing up and completing the onboarding wizard, you land on your practice dashboard. The left sidebar gives you quick access to every section: Overview, Appointments, Patients, Providers, and Settings.

The Overview page is your mission control. It displays:

  • Stats bar — today's appointments, this week's count, total patients, and staff — each with a usage indicator relative to your plan limits
  • Today's schedule — a live list of appointments for the current day with patient name, time, type, provider, and status
  • Upcoming — the next 5 appointments across the following 7 days
  • Platform features (Group+ admins only) — live status indicators for Analytics, WhatsApp, HubSpot, and AI Assistant, with one-click links to configure each
  • Quick actions — shortcuts to create appointments, add patients, manage providers, and navigate to Analytics
  • Plan info — your current plan and subscription status
Customise your layout: Click the Customise button (top-right of the overview) to show, hide, or reorder any dashboard section. Preferences are saved automatically in your browser.
Dashboard overview showing stats and upcoming appointments

First steps checklist

New to Genkō? Work through these steps in order to get fully set up. Expect to spend 10–15 minutes:

  1. Add your services — Go to Settings → Services and create up to two appointment types. Examples: “Initial Consultation (60 min)”, “Follow-up (30 min)”, “Assessment (45 min)”.
    Services define duration and help patients understand what to expect. You can add more later.
  2. Add a provider — Go to Providers and either send an Invite Provider magic link or use Assign myself as Provider. Then click Edit Schedule to define their weekly availability.
  3. Add a patient — Go to Patients and click Add Patient. Full name is required; email and phone help with communication and reminders.
  4. Book your first appointment — Go to Appointments → New Appointment. Select the patient, provider, service, date, and time. Your first booking will help you understand the calendar.
  5. Enable the patient portal (optional) — Go to Settings → Portal to turn on self-service booking. You'll get a public link to share with patients.
    Next step after setup: Invite your team members via Settings → Members so they can help manage appointments.

Appointments

Booking an appointment

Click New Appointment at the top right of the Appointments page. A dialog opens where you fill in:

  • Patient — search by name or email. If the patient doesn't exist, you can add them inline.
  • Provider — only providers with availability on the chosen date are shown.
  • Service — the appointment type, which determines the default duration (adjustable if needed).
  • Date & Time — the date picker highlights days with available slots. Unavailable times are grayed out.
  • Notes — internal notes visible to your team but not shown to the patient.

Once saved, the appointment appears on the calendar. Confirmation emails are sent automatically to the patient and provider (if email notifications are enabled in Settings).

Conflict prevention: Genkō prevents double-booking in real time. If a provider's slot fills while you're booking, you'll be notified.
New appointment dialog with patient, provider, service, and time fields
Demo: walk through the appointment booking flow from the dashboard.

Calendar views

Toggle between three views using the controls at the top of the Appointments page:

  • Day view — focus on a single day, hour by hour. Ideal for detailed scheduling.
  • Week view — see the full week at a glance. Best for spotting gaps and managing multiple providers.
  • Month view — overview of the entire month. Good for long-term planning.

You can also filter by provider to focus on a single practitioner's schedule, or view all providers together. Your last view preference is remembered, so you return to the same view after navigating away.

Use Week view as your default—it balances detail and overview for most practices.

Conflict detection

Genkō automatically prevents double-booking. The system checks every booking in real time:

  • If you try to schedule a provider at a time they're already booked, you'll see an error and the slot will not save.
  • Conflicts are checked on the server, so multiple staff members booking simultaneously are also safe—only one succeeds.
  • The calendar grays out unavailable times, preventing mistakes before they happen.

Buffer minutes (available on Practice plan+) add a gap between appointments so providers have time to prepare, write notes, or take a break. For example, a 60-minute appointment with 10-minute buffer means the next slot can't start until 70 minutes later.

Appointment statuses

Every appointment moves through a lifecycle with four possible states:

  • Scheduled — the appointment is confirmed and appears on the calendar. This is the initial state.
  • Completed — mark this after the visit has taken place. Useful for tracking provider hours and practice productivity.
  • Cancelled — the appointment was cancelled by staff or by the patient via the portal. The time slot is freed up for rebooking.
  • No-show — the patient did not attend. This is separate from cancellation and helps track attendance patterns.

Staff, providers, and admins can update the status from the appointment detail view. Only owners and admins can delete appointments. Cancelled and no-show appointments don't block provider availability—the slot is immediately available for new bookings.

Tracking no-shows: Monitor your no-show rate in the Overview dashboard. High no-show rates may indicate the need for appointment reminders or patient follow-up.

Patients

Adding patients

Go to Patients in the sidebar and click Add Patient. Fill in:

  • Full name — required. Used to identify the patient everywhere in Genkō.
  • Email — optional but recommended. Used to send appointment confirmations and reminders.
  • Phone — optional. Helpful for quick follow-ups or confirmation calls.
  • Date of Birth — optional. Useful for clinical context and age-related notes.
  • Notes — optional. Add any relevant medical history, allergies, or preferences visible to your staff.
Bulk import: Use the Import button to upload a CSV file (in Excel: Save As → CSV) when you're moving existing patient records into Genkō.
Add patient dialog with name, email, phone, and notes fields
Demo: add a patient from the list, then jump into their full profile.

Patient records

Click a patient's name to open their detail page. Here you can:

  • View and update contact information
  • See full appointment history (past, upcoming, cancelled)
  • Add or edit notes visible only to your team
  • Check their usage against your plan limits

Permissions: Owners and admins can edit and delete patient records. Providers and staff have read-only access, protecting patient privacy.

Removing patients: removing a patient who has any history archives them and keeps their record. See Retention and archiving.

Plan limits

Each plan includes a patient limit. When you reach your plan's limit, you'll need to upgrade before adding more patients. Current usage is visible in Settings → Billing.

  • Free — up to 100 patients
  • Solo — up to 250 patients
  • Starter — up to 750 patients
  • Group — up to 2,000 patients
  • Practice — up to 7,500 patients
  • Network — up to 10,000 patients per organization

Clinical records (EMR)

Overview and plans

Genkō keeps each patient's clinical record next to their appointments: notes written on the visit, a coded problem list, vitals, specialty templates, documents and intake answers. The rules that make the record dependable (signing, addenda, retention) are enforced by the database for every user and every integration, not only by the screens.

Who can see it: owners and admins always. Providers and staff need the View patients and Chart notes permissions (Settings → Members → Permissions), and on the appointment page a provider works only on notes for their own appointments. Patients never see a note unless a clinician shares a signed one with them.

Genkō is an EMR for your own practice's records. It does not exchange records with other organizations' systems; you can export coded lists and vitals as FHIR R4 and download any patient's full record.

CapabilityPlan
Clinical notes, signing, addenda, entered in error, print viewStarter+
Problem list with ICD-10-CM search, allergies, medications, vitalsStarter+
Longitudinal record with search and vitals trendStarter+
Record download and visit summariesStarter+
Archiving and record retentionAll plans
Specialty note templatesGroup+
Patient documents (5 GB · 25 GB on Practice · 100 GB on Network)Group+
Pre-visit intake (review and approval on Practice+)Group+
Clinical records over the REST API and MCP (drafts only)Network
Integrations: with the clinical:read and clinical:write scopes (Network), the REST API reads notes, intake, documents and the record, and writes draft notes and draft addenda. See Endpoints.

Clinical notes and signingStarter+

Providers and members with Chart notes write notes on any appointment from the appointment page. Four note types are available:

  • SOAP: Subjective, Objective, Assessment and Plan sections
  • Progress: a free-form narrative update
  • Intake review: comments on a submitted intake form
  • Discharge: an end-of-care summary

From draft to signed

  1. A new note is a draft. Its author can edit or discard it; owners and admins can discard it too.
  2. When it is final, the author signs it after accepting the attestation statement in the sign dialog. Genkō stores that statement, the signer's name and the time with the note.
  3. A signed note can never be edited or deleted, by anyone: the database refuses the change whoever asks, including owners, API keys and Genkō's own server.
  4. To correct or add to a signed note, write an addendum. It is attached to the original, can be kept as a draft, and is signed the same way.
  5. If a note was written for the wrong patient or visit, an owner or admin marks it entered in error with a reason. It stays on the record, labelled, with who did it and when.

Badges show each note's state: Draft, Signed, Signed (migrated) and Entered in error. Notes written before signing existed show Signed (migrated): they were signed in their author's name at their last edit.

Every signed note has a print view (browser print to paper or PDF) with its signed addenda, the attestation and who printed it.

If the signer has a cédula profesional (Mexican professional licence) on their provider profile, the signed note, its print view and the record download show it next to the signature. Genkō copies it at the moment of signing, so a later change to the profile never alters a signed note.

Every note signed from now on carries a SHA-256 fingerprint of its content, computed by the database at the moment of signing (an addendum's also covers its parent's). It appears on the note, its print view, the record download and the patient's visit summary, and owners and admins can verify again to recompute it.

e.firma (Mexico): in practices in Mexico a clinician can sign with their own SAT e.firma instead of the attestation alone. In the sign dialog they choose their .cer and .key files and type the key's password: the key and the password stay in the browser and are never sent to Genkō. Genkō checks that the certificate is a personal e.firma issued by the SAT and valid at signing, asks the SAT whether it has been revoked, and stores the certificate, the signature and the fingerprint; the note then shows Signed electronically (e.firma) · certificate … · RFC …. If the SAT's service does not answer, the note stays a draft. The first e.firma signature registers the clinician's RFC in the practice and a different RFC is refused afterwards (the owner can clear it). The owner can require e.firma for every note and addendum in Settings → Compliance.

API keys, MCP and the AI assistant write drafts only. They can create and edit draft notes and draft addenda; signing and marking a note entered in error always need a signed-in clinician in the dashboard. A request to publish (is_draft: false) is refused with 403 NOTE_SIGN_REQUIRES_CLINICIAN, and any change to a signed note returns 409 NOTE_LOCKED.

Specialty templatesGroup+

Choose a template when you create a note. Its fields appear in the note editor next to the SOAP sections, are saved with the draft and lock with the note when it is signed.

TemplateWhat it records
Dental tooth chartPermanent and primary teeth in FDI or Universal numbering; conditions by surface; procedures as planned or completed.
Mental status examEach MSE domain, risk assessment and clinical impression, with an optional PHQ-9 and GAD-7.
PHQ-9 · GAD-7The questionnaires on their own, for follow-up visits.
Physiotherapy assessmentBody regions, range of motion by joint and movement (side, active and passive, in degrees), outcome measures, findings and plan.
Eye examVisual acuity per eye in Snellen (metres or feet), decimal or logMAR, with logMAR worked out; IOP with the tonometry method; objective and subjective refraction.
Veterinary examSpecies, breed, sex and neuter status, 9-point body condition score, findings and plan. The visit's weight is checked against the species.
Aesthetic treatment recordTreatment types, consent, one row per treated area with product, lot, expiry and amount (units or mL), technique and aftercare.
General notesStructured SOAP visit (review of systems and exam by system), procedure note, consultation/referral and discharge summary — for any practice.
Therapy notesProgress note (interventions, response, progress toward goals, risk), biopsychosocial intake, treatment plan with measurable goals, and a clinician-judged risk assessment (not a validated scale).
More per specialtyDental treatment plan and procedures per tooth · physiotherapy treatment session (techniques, sets and reps) and discharge · vaccination record and surgery/anaesthesia record · laser/energy device parameters and aesthetic consultation · contact lens fitting and glaucoma follow-up.
Nutrition, chiropractic, speech & OTNutrition: ADIME assessment, follow-up, 24-hour recall, plan with daily targets, lab review. Chiropractic: initial exam, treatment visit and re-exam with a clickable spine map, injury narrative. Speech & OT: evaluation, session note with goal data, plan of care, progress re-evaluation. Regions of the body or spine are marked with findings on an interactive map and stay searchable.
Wound careWound assessment (etiology, pressure-injury stage or Wagner grade, location on a body map, measurements and tissue per wound, exudate, periwound, infection signs, ankle-brachial index and sensation, treatment plan), visit / dressing change, debridement record and closure summary. Keep wound photographs in the patient's documents.
MexicoClinical history (historia clínica) in the order Mexican clinical-record rules use: reason for visit, family, non-pathological, pathological and gynaecological-obstetric history (offered for female patients and patients whose sex is other or not recorded), present illness, review of systems, examination, studies, diagnoses, treatment and prognosis; consultation note (nota de interconsulta); referral / transfer note (nota de referencia / traslado) with the receiving establishment and its optional CLUES. Identification and vitals come from the patient record and the note, not asked twice. Fields marked * must be filled before the note can be signed. On by default for practices in Mexico; any practice can switch the group on in Settings → Clinical. The generic consultation/referral template stays available beside it.

Which template groups, and which individual templates, the picker offers is set in Settings → Clinical (new practices choose their practice types in the setup wizard). Turning a group or a template off only hides it for new notes: existing notes, signed notes and API integrations are not affected.

Genkō computes questionnaire totals and severity bands when the note is saved and flags PHQ-9 item 9, so the signed note keeps exactly what the clinician saw. Bands are screening scores, not diagnoses, and each questionnaire is shown with its authors' attribution.

Template answers are searchable from the patient's record, for example a tooth number in either numbering or a PHQ-9 band. Notes that already use a template stay readable on every plan; choosing a template for a new note needs Group or above.

Problem list, allergies, medications and vitalsStarter+

  • Problem list: type a condition and pick an ICD-10-CM suggestion to code it, or keep it as free text. Each entry has a status (active, recurrence, relapse, inactive, remission or resolved) and an optional onset. The ICD-10-CM code set is bundled with Genkō, so what you type is never sent to another service. Descriptions are in English, also in the Spanish interface. An entry keeps the code system it was coded with, and the API also accepts CIE-10 (the WHO ICD-10 used in Mexico, system http://hl7.org/fhir/sid/icd-10) next to ICD-10-CM.
  • Allergies (with severity) and medications (with dose and frequency) are free text.
  • New entries are stamped with who added them and when. Editing the lists is for owners and admins.
  • Vitals on each note: blood pressure, heart rate, respiratory rate, temperature, SpO₂, weight, height and pain (0–10). BMI is worked out from weight and height. Temperature, weight and height can be entered in °C or °F, kg, g or lb, and cm or in. Each value is stored with its LOINC code and UCUM unit and locks with the note.
  • Mexican identifiers (optional): a CURP on the patient (format checked; practices in Mexico see a hint when it is empty, never an error), shown on the patient, the note print view and the record download. Over the REST API the CURP is a clinical field (clinical:read/clinical:write, Network). The owner records the establishment's CLUES and RFC in Settings → Compliance; the CLUES prints in the note header.

The patient's record charts the vitals of signed notes over time, with a table view. The FHIR R4 export in settings includes coded problem-list, allergy and medication entries, and the vitals of signed notes as Observations.

Pre-visit intakeGroup+

When intake is enabled, patients get a link to a pre-visit form before their appointment. It collects:

  • Reason for visit and chief complaint
  • Symptom onset, description and pain scale (0–10)
  • Current medications, known allergies and relevant conditions
  • Confirmation of address, emergency contact and insurance on file

Patients can save a draft and come back, or submit in one go. Once submitted, the intake appears on the appointment for your team.

Taking an intake for the patient: owners, admins and providers (and roles granted View intake forms) can open the appointment's Intake tab and choose Enter intake for the patient. The form is labelled Entered by staff on behalf of the patient and records who entered it. Providers can do this only for their own appointments.

Review (Practice+): owners, admins and providers approve or reject a submitted intake with review notes. An approved intake is final: its answers and review can no longer change. A rejected one can only be reopened for the patient's revision, which then needs a new review.

Patient documentsGroup+

  • Add lab results, imaging, consent forms, referrals and correspondence from the patient's record (Documents), optionally filed under a visit.
  • PDF, JPEG, PNG, HEIC or WebP, up to 25 MB each. Genkō checks the file's real type and a checksum before filing it.
  • Files are kept in private storage. Opening one creates a link that works for 60 seconds.
  • A document is never edited or deleted. An owner or admin can void it with a reason; it stays listed as voided.
  • Storage per organization: 5 GB on Group, 25 GB on Practice, 100 GB on Network.

Owners, admins and members with View patients and Chart notes can see and add documents. Patients cannot upload. Over the REST API, documents can be listed and read (Network, clinical:read), not uploaded.

Longitudinal recordStarter+

Open View patient record from an appointment to see the patient's whole history in order: every appointment with its notes and addenda, intake answers (Group+), vitals and the documents filed under each visit.

The search box finds words in note text, template answers, visit types, providers and booking notes. Every word must match; case and accents are ignored. Members without Chart notes do not see notes, and search skips them too.

Retention and archivingAll plans

  • Remove on a patient deletes them only if they have no history. A patient with any appointment, note, intake or document is archived instead: hidden from lists, upcoming appointments cancelled, the record kept and still downloadable.
  • An archived record is kept until its Kept until date: the last clinical activity plus your retention period. The owner sets the period in Settings → Compliance → Record retention (5 to 30 years, 10 by default; admins see it read-only). For minors the period starts at the age of majority, and where Genkō knows a legal minimum for the practice's country, the period is never shorter.
  • Owners and admins see archived patients in the Archived view, with the archive date and Kept until, and can restore them. An archived patient cannot get new appointments, notes or documents until restored, and does not count towards your plan's patient limit. In the patient export, archived patients keep their rows, with the archive date in an Archived column (CSV/Excel) and active: false (FHIR); deactivated providers are exported with active: false too.
  • A provider with appointment history is deactivated instead of deleted: off the booking portal, history intact.

Genkō never deletes a retained record automatically. An organization with appointment history can be closed but not deleted.

Record download and visit summariesStarter+

Record download

Download record on the patient's record page opens their whole record as a printable page. Print it, or save it as a PDF from the browser's print dialog. It contains:

  • Demographics, contact, insurance and the practice's notes on file
  • Problem list with codes, allergies, medications and vitals
  • The full appointment history, every signed note with its addenda (entered-in-error notes marked) and template answers
  • Approved intake answers, if you can view intake forms
  • The index of documents (titles, categories and dates, without the files)

Drafts and booking notes are left out. Archived patients can be downloaded too. Owners, admins and members with View patients and Chart notes can download, and each download is recorded in the audit log.

Visit summaries in the patient portal

On a signed note, its signer or an owner or admin can choose Share with patient:

  • The patient sees a read-only visit summary in their portal: the note, its signed addenda, template answers, vitals and who signed it.
  • Drafts, unsigned addenda and other notes stay private.
  • Stop sharing removes it from the portal; a note marked entered in error leaves the portal on its own.
  • Each share and each stop is recorded in the audit log. The patient is not notified automatically.

Providers

Adding providers

There are two common ways to add providers:

  • Invite Provider — send a magic-link invite from the Providers page when you're onboarding a teammate.
  • Assign myself as Provider — owners can add themselves if they also see patients directly.

Invited providers join with the Provider role, then appear in your scheduling views as soon as they set their specialty and availability.

Use specialties and bios to help staff and patients choose the right clinician, especially when self-booking is enabled.
Invite provider dialog with full name and email fields

Setting availability

Open a provider card and click Edit Schedule to define their weekly availability. Genkō uses this to show only bookable slots when creating appointments or in the patient portal.

How it works:

  • Set availability for each day of the week (Monday through Sunday)
  • For each working day, specify start time and end time (e.g. "9:00 AM - 5:00 PM")
  • Providers not working a day can be marked as unavailable
  • The system blocks bookings outside these hours
Example: If Dr. Smith works Monday–Thursday 9am–6pm and Friday 9am–12pm, you'd set availability for each day accordingly.
Provider schedule editor with weekday toggles and start and end time fields
Demo: preview the provider invite flow, then update an existing clinician's weekly schedule.

Advanced scheduling rules

Practices on the Practice plan and above can configure fine-grained rules per provider to enforce your scheduling policies:

  • Buffer minutes — adds a gap between consecutive appointments. Example: 60-min appointment + 10-min buffer = next slot starts 70 minutes later. Great for note-taking or prep time.
  • Max appointments per day — caps the total number of bookings for a provider on any given day. Example: max 8 appointments prevents overbooking.
  • Booking window — limits how far in advance patients can book. Example: “up to 60 days ahead” prevents far-future bookings. Useful for practices that want to keep scheduling flexible.
  • Day-of-week restrictions — limit which days a provider is bookable via the patient portal (staff can still book them on restricted days). Useful for specialists who work limited hours.
Best practice: Start simple. Add advanced rules only when you identify a scheduling pain point—for example, add buffer minutes if providers need admin time between patients.

Services

Appointment types

Services (also called appointment types) define the kinds of visits your practice offers. Each service has:

  • Name — the appointment type displayed to staff and patients (e.g. "Initial Consultation")
  • Duration — default length in minutes. When you book an appointment with this service, the duration pre-fills (adjustable per booking if needed).
  • Description — optional. Explains what the appointment includes. Shown to patients in the portal to set expectations.
Example services: "Initial Consultation (60 min)", "Follow-up Visit (30 min)", "Assessment (45 min)", "Lab Work (15 min)"

Managing services

Go to Settings → Services to create, edit, or archive services. You can:

  • Create new services — click Add Service and fill in name, duration, and optional description.
  • Edit services — click the pencil icon next to any service to change its details.
  • Archive services — hide old or unused services without deleting them. Archived services can't be booked but appear in historical records.

Permissions: Owners and admins can manage services; providers and staff have read-only access.

Plan details: Free includes up to two services. Solo and above unlock multiple services, optional appointment fees, Google Calendar sync, and Stripe-powered paid booking. Practice and above also add per-service buffer time.

Services page showing appointment types, durations, and add and edit actions
Demo: create a new service with duration, description, pricing, and brand color.

Team Members

Inviting team members

Go to Settings → Members and click Invite Member. Enter their full name, email, and role. Here's how it works:

  • An invitation email is sent to the team member's inbox with a magic link (no password needed)
  • They click the link and gain immediate access to your practice
  • Invites expire after 72 hours for security. You can review and revoke pending invites from the Members page.
  • Team member limits are enforced per your plan (see Billing below)
Tip: Invite team members with the lowest required role first. You can always promote them later if needed.
Invite team member dialog with full name, email, and role fields
Members page showing team list and pending invitations
Demo: send a member invite and confirm it appears in Pending invitations.

Roles & permissions

Genkō has four staff roles. Each role has different access levels:

  • Owner — full access to everything, including billing, organization settings, and team management. Each organization has exactly one owner. Can transfer ownership or delete the organization.
  • Admin — same access as owner except cannot transfer ownership, delete the organization, access advanced compliance features, connect or change the Stripe payout account, or grant, change or remove the admin role (only the owner can). Admins always have full access to patients, scheduling and clinical records; to limit what someone can see, give them the Provider or Staff role instead. Good for practice managers. Admins may also be delegated permission management for providers and staff if the owner allows it. Three capabilities — managing API keys, turning the dashboard AI assistant on or off, and managing webhooks — are off for admins by default; the owner grants them from the Permissions page.
  • Provider — can view and manage their own schedule and appointments. Can read patient records but cannot modify members, billing, or organizational settings. Good for clinicians who don't manage the practice.
  • Staff — can manage appointments and read patient/provider information. Cannot modify members, billing, settings, or provider information. Good for schedulers and administrative assistants.

Team limits: Your plan includes a maximum number of staff members, and the owner counts toward that limit. Owners and admins can change a provider's or staff member's role or remove them from the Members page at any time; only the owner can do so for an admin.

Adding someone who already has a Genkō account: they receive an invitation and join only after accepting it. The invitation is sent the same way whether or not the address already has an account.

When someone leaves: removing a member, demoting an admin, or deleting an account revokes the API keys that person created and disables the webhook endpoints they set up. Review a disabled endpoint and rotate its signing secret before turning it back on.

Provider and staff permissions are enforced everywhere: what you switch off on the Permissions page is refused by the dashboard, the AI assistant and the database alike.

Patient privacy: Each role is restricted to appropriate information. Patients cannot see staff details—only provider names and specialties (if in the patient portal).

Permissions & RBAC

The Permissions page is where owners and delegated admins manage role-based access control (RBAC) for your practice.

  • Role permission matrix — review and adjust what Admin, Provider, and Staff can do across Patients, Appointments, Providers, Services, and Settings.
  • RBAC delegation — owners can delegate permission management to trusted admins without giving them full ownership rights.
  • Granular controls — enable or restrict specific workflows like appointment deletion, patient export, and billing access.
  • Admin capability toggles — API key management, the dashboard AI assistant, and webhook management are off for admins until the owner turns them on for the admin role here.

Changes made on the Permissions page take effect immediately for active users. This helps you keep practice operations secure while still letting the right people move quickly.

Best practice: keep privileges as narrow as possible. Give staff only the permissions they need for scheduling and patient support, while reserving billing and team management for admins and the owner.
Permissions page showing role matrix and RBAC delegation settings
Demo: open the Permissions page, view the role matrix, and toggle RBAC delegation for an admin.

Plans & Billing

Available plans

Genkō offers six tiers designed to grow with your practice. Every plan includes appointment scheduling with real-time conflict detection, provider availability management, role-based access control, and a patient portal (view-only on Free; patients can book for themselves from Solo).

PlanMonthlyYearlyPatientsStaffOrganizations
Free$0—10031
Solo$49$49025041
Starter$79$79075081
Group$139$1,3902,000155
Practice$279$2,7907,5004010
Network$549$5,49010,000Unlimited25

Advanced features unlock as you scale, and each plan includes everything in the plans below it:

  • Solo and above: Multiple services, appointment fees and Stripe payment collection, patient self-service booking, email booking confirmations, Google Calendar sync (each provider connects their own), and a 90-day audit log
  • Starter and above: Clinical notes, longitudinal patient records, WhatsApp confirmations and reminders patients can reply to (confirm, or get a link to reschedule or cancel), and HubSpot CRM sync
  • Group and above: Analytics, pre-visit intake forms, the dashboard AI assistant, and the REST API and MCP API (read-only, 1 key of each kind)
  • Practice and above: Email and SMS appointment reminders, intake review and approval, the patient portal AI assistant, the embeddable website chat widget, advanced scheduling rules, and read + write access on the REST and MCP APIs (3 keys of each kind)
  • Network: Clinical records, patient deletion, batch endpoints and signed webhooks on the REST API, admin-scope keys with no key limit, up to 25 organizations, a 365-day granular audit log, dedicated onboarding, and SLA-backed support
Yearly discount: Pay for 10 months, get 12 — about 17% off (two months free) on annual plans.

Upgrading or downgrading

Go to Settings → Billing to manage your plan. Only the organization owner can open this page. Your plan belongs to you as owner: new organizations you create start on your highest current plan and share its subscription, so a plan change applies to every organization on that subscription (patient and staff limits are still counted per organization).

  • Upgrade: Click Upgrade to <plan> on the plan you want. On your first paid plan you are sent to Stripe Checkout; if you already have a subscription, you confirm the change and it takes effect immediately, with the prorated difference charged right away.
  • Downgrade: Click Downgrade to <plan> on a lower paid plan. It also takes effect immediately and Stripe prorates it. It is blocked while your current usage (patients or staff) is above the new plan's limits, so reduce it first. There is no Downgrade button for Free: cancel the subscription from the billing portal (below).
  • Billing interval: Use the Monthly / Yearly toggle above the plans; the interval you have selected is used when you choose a plan.
Before downgrading: Check your current usage. If you have 600 patients and downgrade to Solo (250 limit), you'll need to reduce patient count or upgrade again.

Billing portal

After subscribing, manage billing details through the Stripe billing portal accessible from Settings → Billing → Manage Subscription:

  • Update your payment method
  • View and download invoices and receipts
  • Cancel your subscription (access continues until period end)
  • View billing history and upcoming charges

Permissions: Only the organization owner can open billing settings (plan, payment methods and invoices); admins cannot. This keeps your payment information with the person who owns the subscription.

Patient Portal

Enabling the portal

The patient portal lets patients self-book appointments without staff involvement, reducing administrative overhead and improving patient convenience.

To enable:

  1. Go to Settings → Portal
  2. Toggle Enable Patient Portal to on
  3. Your practice gets a unique public booking URL: https://www.getgenko.com/portal/your-slug
  4. Share this link with patients via email, website, appointment confirmations, or anywhere else
Portal URL: Your slug is based on your organization name. You can customize it in portal settings.
Patient portal settings page with enable toggle and public booking URL

Patient self-booking

When patients visit your portal link, they follow a simple flow:

  1. Browse offerings: See available services with descriptions and providers with specialties/bios
  2. Select service & provider: Choose which service they want and who they prefer (or auto-assign)
  3. Pick date & time: Calendar shows only available slots based on provider availability and scheduling rules. Before a day is picked, each day is coloured by availability: green has openings, amber has only a few times left, and greyed-out days have nothing to book
  4. Enter contact info: First name, last name, email, phone. Account creation is not required.
  5. Confirmation: Patient receives an email with appointment details; booking appears in your calendar immediately

Smart matching: If the patient's email already exists in your database, the booking automatically links to their existing record. Otherwise, a new patient record is created.

Reduce no-shows: Reminders go out the day before: WhatsApp from Starter (once enabled in Settings → Notifications), and email and SMS (to patients who opted in) from Practice. Bookings send an email confirmation on Solo and above, and a WhatsApp confirmation from Starter (SMS from Practice) to patients who opted in.
Demo: open the portal preview from settings, then step into the public booking flow.

Booking widget & book button

Patients can book from your own website, not only from your portal. Both options open the same booking flow as your portal, with your services, providers, availability colours and scheduling rules.

Go to Settings → Integrations → Online booking. Optionally preselect a service and/or provider, then copy the code. Your patient portal must be enabled.

Booking widget (embedded)

Paste this where the booking flow should appear. It loads in a frame on your page and resizes itself to fit each step:

<div data-genko-book="your-slug"></div>
<script src="https://www.getgenko.com/book.js" async></script>

If your website builder removes script tags, use a plain iframe instead (fixed height):

<iframe src="https://www.getgenko.com/embed/book/your-slug" title="Book an appointment" loading="lazy" style="display:block;width:100%;height:820px;border:0"></iframe>

When a patient books, the widget fires a genko:booked DOM event on its container (event.detail.paymentRequired tells you whether payment is still due). Sign-in, password reset and online payment open in a new tab. The widget always runs signed out and never uses a visitor's Genkō session.

Book button

A styled link that opens your booking page in a new tab — no script, so it works in any website builder, email signature or link-in-bio page. Choose its text, colours, style, shape, size and the booking page language in the builder:

<a href="https://www.getgenko.com/portal/your-slug/book" target="_blank" rel="noopener" style="display:inline-block;padding:12px 24px;border-radius:8px;border:2px solid #4f46e5;background:#4f46e5;color:#ffffff;font:600 16px/1.2 system-ui,-apple-system,BlinkMacSystemFont,Roboto,Helvetica,Arial,sans-serif;text-decoration:none;cursor:pointer">Book an appointment</a>
Online booking, the widget and the button are available on Solo and above.

Notifications

Owners and admins choose which WhatsApp messages the practice sends and which actions a patient may take by replying, in Settings → Notifications. Nothing changes until you change it: every message and every reply action is on by default.

Which messages go out

The WhatsApp switch (Starter and above, with the patient-data acknowledgement) turns messaging on for the practice; it moved here from Settings → Integrations. With it on, each kind has its own switch (Starter and above):

MessageWhen it is sent
Booking confirmationRight after an appointment is booked
Day-before reminderThe morning before the appointment
Same-day reminderA few hours before the appointment
Reschedule noticeWhen an appointment is moved
Cancellation noticeWhen an appointment is cancelled

Switching a kind off stops that message on WhatsApp and its text-message (SMS) fallback on plans that have one (Practice and above). The Email section has its own switch for each appointment email: booking confirmation, day-before reminder, same-day reminder, reschedule notice and cancellation notice. Email switches are available on every plan and can only stop an email: a patient who opted out of email never receives one, whatever is set. Account, security, invitation, receipt and visit-summary emails cannot be switched off.

The setting applies to the whole practice, not to individual providers or services.

What patients can do by reply

From the Group plan, three switches decide which actions a patient may take from a reply, a message button, a list choice or the secure link:

  • Confirm the appointment
  • Reschedule it (patients pick a new time from the open slots)
  • Cancel it

On Starter and Solo these are locked on: patients can use all three, as before. The plan is checked each time a message arrives, so a downgrade takes effect straight away.

The secure link and what patients see

Messages and emails include a personal link, /portal/your-practice/appointment/…, that opens one appointment without a login so the patient can confirm, choose a new time or cancel it.

  • An action that is switched off is hidden on the link page, and the page explains that the practice handles it directly.
  • If a patient replies with a switched-off action, nothing changes. They get a polite message with your practice name and, if you have set them, your phone number and email. The link is included only when another action is still allowed.
  • Patients signed in to the patient portal can still cancel there, up to 24 hours ahead; these switches do not control that.
Changes are recorded in the audit trail with the keys that changed. There is no REST or MCP endpoint for these settings; they are managed in the dashboard.

Analytics & Insights

The Analytics dashboard gives you real-time visibility into your practice's performance. Track appointment patterns, provider productivity, no-show rates, patient growth, and more across any date range. All data is exportable to CSV or Excel for deeper analysis or integration with external reporting tools.

Plan requirement: Analytics is available on the Group plan and above. Free, Solo, and Starter plans do not have access to analytics.

Dashboard overview

The Analytics page is located at Overview → Analytics (or directly from the quick-links in your dashboard). It displays seven key performance indicators at the top, followed by interactive breakdowns of your data.

Main section: Shows a preview of data for the selected date range. Click View full analytics to expand and see all available metrics and filters.

Date range and presets

Analytics support two ways to select a date range:

  • Presets: Quick shortcuts for common periods — Today, Yesterday, Last 7 days, Last 30 days, Last 90 days, This month, Last month, This year
  • Custom range: Pick any start and end date to compare specific periods (e.g., comparing two months or comparing performance before/after a process change)

The dashboard updates instantly when you change the date range.

Key performance indicators

The top section displays seven KPIs for your selected period:

  • Total appointments: Count of all appointments in the period (including completed, no-shows, and cancelled)
  • Completed appointments: Count of successfully completed visits
  • No-shows: Count of appointments marked as no-show
  • Cancelled appointments: Count of cancelled visits (by staff or patient)
  • Completion rate: Percentage of total appointments that were completed (completed ÷ total)
  • No-show rate: Percentage of total appointments that were no-shows (no-shows ÷ total)
  • New patients: Count of new patient registrations in the period

Breakdowns and filters

Below the KPIs, you can view data broken down by:

  • By day: A line chart showing appointment volume and completion rates for each day in the selected range
  • By appointment type: See which services are most popular and their no-show rates
  • By provider: Compare provider productivity, completion rates, and no-show rates
  • By status: Breakdown of appointments by status (completed, no-show, cancelled, scheduled)

Each breakdown is interactive — click on a segment to drill down or filter the entire dashboard by that value.

Export data

All analytics data can be exported for external analysis, reporting, or integration:

  1. Click Export in the top right
  2. Choose format: CSV (opens in spreadsheets like Excel) or Excel (creates a downloadable .xlsx file)
  3. The file includes all KPIs and breakdown data for the selected period
External analysis: Export to CSV or Excel, then use tools like Google Sheets, Power BI, or Tableau to create custom reports or visualizations beyond what's available in Genkō.

Integrations

Google Calendar sync

Each provider connects their own Google Calendar. Genkō then syncs the appointments assigned to that provider to that calendar, so a provider's schedule is in one place whether they check Genkō or Google Calendar.

How to connect (each provider does this for themselves):

  1. Go to Settings → Integrations
  2. Click Connect my calendar and sign in with the Google account you want to sync to
  3. Grant Genkō permission to create and modify events in your calendar — sync starts with the next appointment

Who can connect: owners, admins and providers who have a provider profile in the practice. Staff cannot, and neither can an owner or admin without a provider profile — there would be nothing to sync to their calendar.

What syncs (one way, from Genkō to Google): appointments booked after you connect, their time changes, and cancellations. If an appointment is reassigned to another provider, the event moves to that provider's calendar. A provider who has not connected a calendar gets no events; there is no shared practice calendar.

What an event contains: by default only Appointment — your practice name and the time. No patient name, no notes, and the patient is not invited by email. An admin can turn on Full patient detail in calendar events in Settings → Integrations; events then include the patient's name, the service and the notes, and the patient receives a calendar invitation.

A provider's personal Google account is not covered by your practice's agreements. Turn on full patient detail only if your compliance policy allows patient information there.

Plan requirement: Google Calendar sync is available on the Solo plan and above. It is checked when you connect and on every sync; if a plan is downgraded, syncing pauses and connections are kept.

Disconnect: everyone can disconnect their own calendar from Settings → Integrations; owners and admins can also see and disconnect any member's. A calendar is disconnected automatically when its owner is removed from the practice, changes to a role that cannot connect one, or deletes their account. Past events in Google Calendar remain untouched.

Integrations page showing the Google Calendar connection card

REST API (v1)

What is the REST API?

The Genkō REST API lets your ERP, EHR, billing system or scripts read and write the same scheduling data you see in the dashboard: patients, appointments, services and provider schedules. It speaks JSON over HTTPS at https://www.getgenko.com/api/v1 and is authenticated with an API key.

It follows the same data rules as MCP (next section), but uses its own keys: a REST API key works only on /api/v1 and an MCP key only on /api/mcp. REST API keys are created in Settings → Developers → REST API keys.

A machine-readable description of every endpoint, schema and error is served as OpenAPI 3.1 at https://www.getgenko.com/api/v1/openapi.json. Import it into Postman or Insomnia, or generate a client library in your language. It is generated from the API itself, so it is always current.

Plan requirements: reading is available on Group and above; writing on Practice and above; clinical records, patient deletion and the batch endpoints on Network. The plan is checked on every request, so a downgrade takes effect within a minute: the key is not revoked, the endpoints simply stop being available.

Authentication and scopes

Send the key in the Authorization header. A key in the URL is rejected, because URLs end up in logs; if you ever put a key in one, revoke it.

curl "https://www.getgenko.com/api/v1/patients?limit=10" \
  -H "Authorization: Bearer genko_YOUR_KEY_HERE"
REST API keys only: an MCP key (genko_mcp_…) sent to /api/v1 is refused with 401 UNAUTHORIZED and details.key_kind: "mcp". Key limits are counted per kind: Group 1, Practice 3, Network unlimited REST API keys, in addition to your MCP keys.

Keys expire after 365 days unless you choose an earlier date. A request outside a key's scopes returns 403 FORBIDDEN with details.required_scope naming what was missing:

  • Read key: scheduling:read, patients:read, services:read, providers:read (Group+)
  • Read & write key: adds scheduling:write, patients:write, services:write, providers:write (Practice+)
  • Admin key: adds patients:delete, clinical:read and clinical:write (Network)

When you create a key you can also pick individual scopes instead of a preset, restrict it to specific IP addresses (an allowlist of addresses or CIDR ranges; requests from anywhere else get 403 FORBIDDEN), and give it its own requests-per-minute limit (1–10,000; writes get a fifth of it, and the organization-wide ceiling still applies). A scope your plan does not include cannot be selected.

GET /api/v1 returns who the key belongs to, the scopes that work right now and the limits that apply.

Clinical fields: name, contact details and date of birth are available to any key that can read patients. Notes, allergies, conditions, medications, insurance, emergency contact and address are clinical: they are returned, and can be written, only with clinical:read / clinical:write. Allergy, condition and medication entries are free text (name) with an optional coding: code + system together, plus display, status, onset, abatement (conditions and medications), recorded_at and recorded_by. Code conditions with ICD-10-CM (system: "http://hl7.org/fhir/sid/icd-10-cm", dotted code such as J45.909). Each list replaces the stored one.

Requests, responses and errors

Every response is JSON in the same envelope, with Cache-Control: no-store and an X-Request-Id you can quote to support:

{ "data": { … }, "error": null, "meta": { "request_id": "…" } }
{ "data": null, "error": { "code": "NOT_FOUND", "message": "Patient not found" }, "meta": { "request_id": "…" } }
  • Send Content-Type: application/json. Bodies are limited to 1 MB.
  • Unknown fields and unknown query parameters are rejected with 400 VALIDATION_ERROR instead of being ignored, so a typo never silently does the wrong thing.
  • Timestamps are ISO-8601 in UTC. Ranges are half-open: from is inclusive and to is exclusive, so adjacent ranges never double-count.
  • Branch on error.code, never on the message: VALIDATION_ERROR, UNAUTHORIZED, FORBIDDEN, PLAN_LIMIT, NOT_FOUND, CONFLICT, DUPLICATE, INVALID_TRANSITION, DAILY_LIMIT_REACHED, OUTSIDE_AVAILABILITY, IN_THE_PAST, PRECONDITION_FAILED, RATE_LIMITED, IDEMPOTENCY_KEY_REUSED, IDEMPOTENCY_IN_PROGRESS, INTERNAL. Published codes are never renamed.
  • Plan names: enterprise is the stable identifier of the Network plan (it predates the rename). It is what details.required_plan in a PLAN_LIMIT error and the plan field of GET /api/v1 contain; those values are never renamed.
  • A record that belongs to another organization is indistinguishable from one that does not exist: both are 404.
  • Idempotency: send an Idempotency-Key header (1-128 characters) on any POST, PATCH or DELETE. A retry with the same key and the same request returns the original response (marked Idempotent-Replayed: true) without doing the work again; the same key with a different request is 422 IDEMPOTENCY_KEY_REUSED; a duplicate that arrives while the first is still running is 409 IDEMPOTENCY_IN_PROGRESS. Only successful responses are stored for 24 hours, so a failed request can simply be retried. If the idempotency store is unavailable the API answers 503 rather than risk a double write: retry shortly. Creating a record with an external_id that already exists returns the existing record, which is durable even without the header.
  • Rate limits are per key per minute: Group 60 reads / 12 writes, Practice 300 / 60, Network 600 / 180, plus an organization-wide ceiling shared by all keys. Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset; over the limit you get 429 with Retry-After.

Pagination and syncing

Lists use keyset cursors, never offsets, so page 1,000 costs the same as page 1 and rows are never skipped or repeated while data changes. Pass limit (default 100, maximum 500) and follow meta.next_cursor until meta.has_more is false. A cursor is bound to its filters; changing them returns 400 INVALID_CURSOR, so restart without a cursor.

  • Initial backfill: page GET /patients?limit=500 and GET /appointments?limit=500 to the end.
  • Incremental sync: remember the highest updated_at you saw and next time request ?updated_since=<that value minus 60 seconds>. Always overlap by 60 seconds and upsert by id or external_id: a transaction that began before your last read can commit after it.
  • Deletes do not appear in updated_since. Subscribe to the patient.deleted and appointment.deleted webhooks, and reconcile now and then by paging ?fields=id&limit=500 and comparing with your copy. Archives do appear (archived_at is set and updated_at moves), but archived patients drop out of lists unless you pass include_archived=true; patient.archived and patient.restored announce them.
  • external_id is your identifier on patients, appointments, services and providers; it is unique within your organization and can be used to filter, create and upsert.

Endpoints

All paths are relative to /api/v1. Reads need the matching :read scope and writes the :write scope unless noted.

Patients

  • GET /patients — list, with q (3+ characters), email, phone, mrn, external_id, updated_since, sort, fields
  • GET /patients/:id — one patient
  • POST /patients — create; an existing external_id returns that patient
  • PATCH /patients/:id — change only what you send; null clears a field
  • POST /patients/upsert — create or update by external_id
  • POST /patients/batch — up to 500 patients per call with a per-item result (Network)
  • DELETE /patients/:id — clinical records are retained, so a patient with appointments or clinical data is archived (hidden from lists, upcoming appointments cancelled, record kept until retain_until); a patient with no history is deleted. The response says which (result). cascade is deprecated and ignored (behaviour change, October 2026). Network, patients:delete

Appointments

  • GET /appointments — list, with provider_id, patient_id, service_id, status, from, to, updated_since, external_id, expand
  • GET /appointments/:id — one appointment
  • POST /appointments — book (conflict-checked); notify, external_id, and status for importing history
  • PATCH /appointments/:id — change status or reschedule, optionally guarded with If-Match: <updated_at> (412 if it changed)
  • POST /appointments/:id/cancel — cancel; repeating it is harmless
  • POST /appointments/batch — up to 500 appointments per call, upserted by external_id (Network)

Services and providers

  • GET|POST /services — list and create service types
  • GET|PATCH|DELETE /services/:id — read, change, delete; deleting one that appointments use needs ?force=true
  • GET /providers, GET /providers/:id — name, specialty, timezone, weekly hours and booking rules (never user ids or emails)
  • PATCH /providers/:id — update hours, booking rules and external_id (providers are created by invitation in the dashboard)
  • GET /providers/:id/availability — ?from=&to=&duration=&service_id=: free slots over up to 31 days from one query

Clinical records (Network, Admin key)

  • GET /patients/:id/record — a patient's visits with their notes and intake forms
  • GET /patients/:id/documents — a patient's documents (lab results, imaging, consents, referrals, correspondence): metadata only, newest first, voided documents included and marked. Documents are uploaded by staff in the dashboard; there is no upload over the API
  • GET /patients/:id/documents/:documentId — one document with a signed download url valid for 60 seconds (url_expires_at): fetch it at once, never store it. Every read is written to the audit log (document.viewed)
  • GET|POST /appointments/:id/notes — list and create clinical notes. A note may carry vitals (blood pressure, heart and respiratory rate, temperature, SpO₂, weight, height, pain 0–10): send numbers or { value, unit }; Genkō stores each with its UCUM unit and LOINC code and computes BMI. Vitals are locked with the note once it is signed. A note may also be written with a specialty template: template_key (dental.tooth-chart@1, mental-health.mse@1, mental-health.phq9@1, mental-health.gad7@1, physio.assessment@1, optometry.eye-exam@1, vet.exam@1, medspa.treatment@1) plus its template_data, validated against that template's schema (NoteTemplateData in the OpenAPI document); Genkō computes the PHQ-9 and GAD-7 totals and severity bands and the logMAR of each visual acuity, checks a vet.exam@1 note's vitals.weight against its species, and the template is locked with the note once it is signed
  • GET|PATCH|DELETE /appointments/:id/notes/:noteId — read a note; edit or discard a draft (a note signed in the dashboard is locked: 409 NOTE_LOCKED)
  • POST /appointments/:id/notes/:noteId/addenda — add a draft addendum to a signed note. Keys write drafts only; clinicians sign in the dashboard
  • GET /appointments/:id/intake, POST /appointments/:id/intake/review — read a submitted intake form, approve or reject it

Webhooks (Network, webhooks:manage)

  • GET|POST /webhooks — list and create endpoints (the signing secret is returned once)
  • GET|PATCH|DELETE /webhooks/:id — read, change (URL, events, active) or delete an endpoint
  • POST /webhooks/:id/rotate-secret — new secret; ?grace_hours= keeps the old one valid meanwhile
  • POST /webhooks/:id/test — send a signed test event now and see the answer
  • GET /webhooks/:id/deliveries — delivery history with status, attempts and last error
  • POST /webhooks/:id/deliveries/:deliveryId/redeliver — send a finished delivery again

Behaviours worth knowing

  • Notifications: booking, rescheduling and cancelling send the usual patient email/SMS and update Google Calendar. If your system already messages patients, send notify: false (or ?notify=false when cancelling). Batches never notify.
  • Status rules: scheduled → confirmed → completed, and cancelled or no_show. Finished appointments stay finished: changing them returns 409 INVALID_TRANSITION. A time and a status change are sent as separate requests.
  • Batches answer 200 with one result per item (created, updated or error with a stable code). One bad item never fails the others, and re-sending the same batch is safe.
  • Clinical access is audited: every read and write of clinical records writes a detailed audit-log entry (ids only, never the content).

Request and response examples

Each example shows the exact JSON your integration sends and receives. The values are illustrative and every request also carries the Authorization header shown earlier. Open a group to see its calls.

Account1
GET/

Request

GET /api/v1

Response 200 OK

{
  "data": {
    "version": "v1",
    "organization": {
      "id": "6a1d3f58-92c4-4b7e-8d05-f3e27c9b1a40",
      "name": "Ruiz Physio",
      "slug": "ruiz-physio",
      "timezone": "America/Mexico_City"
    },
    "plan": "enterprise",
    "key": {
      "id": "9e05c7b2-14d8-4a36-b7f9-20c8e5a1d463",
      "name": "ERP sync",
      "expires_at": "2027-09-19T00:00:00.000000+00:00",
      "ip_restricted": true,
      "scopes": [
        "patients:read",
        "patients:write",
        "scheduling:read",
        "scheduling:write",
        "services:read",
        "services:write",
        "providers:read",
        "providers:write"
      ],
      "declared_scopes": [
        "patients:read",
        "patients:write",
        "scheduling:read",
        "scheduling:write",
        "services:read",
        "services:write",
        "providers:read",
        "providers:write"
      ]
    },
    "limits": {
      "requests_per_minute": {
        "read": 600,
        "write": 180,
        "organization": 3000
      },
      "page_size": {
        "default": 100,
        "max": 500
      },
      "max_body_bytes": 1048576
    }
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
Patients7
GET/patients

Request

GET /api/v1/patients?q=garcia&limit=2

Response 200 OK

{
  "data": [
    {
      "id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
      "external_id": "ERP-P-1042",
      "medical_record_number": "MRN-20418",
      "full_name": "Ana García López",
      "email": "ana.garcia@example.com",
      "phone": "+14155550123",
      "date_of_birth": "1988-04-12",
      "sex": "female",
      "preferred_language": "es",
      "sms_opt_in": true,
      "archived_at": null,
      "created_at": "2026-09-01T15:04:05.123456+00:00",
      "updated_at": "2026-09-19T10:31:47.482913+00:00"
    },
    {
      "id": "2b9e6d40-7c15-4a83-b0f6-e3a1d8c47952",
      "external_id": null,
      "medical_record_number": null,
      "full_name": "Marco García",
      "email": null,
      "phone": "+14155550188",
      "date_of_birth": null,
      "sex": null,
      "preferred_language": "en",
      "sms_opt_in": false,
      "created_at": "2026-09-01T15:04:05.123456+00:00",
      "updated_at": "2026-09-01T15:04:05.123456+00:00"
    }
  ],
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05",
    "next_cursor": "eyJ2IjoxLCJzIjoidXBkYXRlZF9hdCIsImQiOiJhc2MifQ.q8Zt1c",
    "has_more": true,
    "limit": 2
  }
}
GET/patients/8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23

Request

GET /api/v1/patients/8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23?fields=id,full_name,email

Response 200 OK

{
  "data": {
    "id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
    "full_name": "Ana García López",
    "email": "ana.garcia@example.com"
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
POST/patients

Request· Required fields: full_name

POST /api/v1/patients
Idempotency-Key: erp-patient-1042-create
Content-Type: application/json

{
  "full_name": "Ana García López",
  "external_id": "ERP-P-1042",
  "email": "ana.garcia@example.com",
  "phone": "(415) 555-0123",
  "date_of_birth": "1988-04-12",
  "preferred_language": "es",
  "sms_opt_in": true
}

Response 201 Created

{
  "data": {
    "id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
    "external_id": "ERP-P-1042",
    "medical_record_number": "MRN-20418",
    "full_name": "Ana García López",
    "email": "ana.garcia@example.com",
    "phone": "+14155550123",
    "date_of_birth": "1988-04-12",
    "sex": "female",
    "preferred_language": "es",
    "sms_opt_in": true,
    "archived_at": null,
    "created_at": "2026-09-01T15:04:05.123456+00:00",
    "updated_at": "2026-09-19T10:31:47.482913+00:00"
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05",
    "created": true
  }
}

Response 200 OK (same external_id sent again)

{
  "data": {
    "id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
    "external_id": "ERP-P-1042",
    "medical_record_number": "MRN-20418",
    "full_name": "Ana García López",
    "email": "ana.garcia@example.com",
    "phone": "+14155550123",
    "date_of_birth": "1988-04-12",
    "sex": "female",
    "preferred_language": "es",
    "sms_opt_in": true,
    "archived_at": null,
    "created_at": "2026-09-01T15:04:05.123456+00:00",
    "updated_at": "2026-09-19T10:31:47.482913+00:00"
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05",
    "created": false
  }
}
PATCH/patients/8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23

Request

PATCH /api/v1/patients/8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23
Content-Type: application/json

{
  "phone": "+14155550199",
  "email": null,
  "conditions": [
    {
      "name": "Asthma",
      "code": "J45.909",
      "system": "http://hl7.org/fhir/sid/icd-10-cm",
      "display": "Unspecified asthma, uncomplicated",
      "status": "active",
      "onset": "2019-04"
    }
  ]
}

Response 200 OK (conditions needs clinical:write; with clinical:read the clinical fields are returned too)

{
  "data": {
    "id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
    "external_id": "ERP-P-1042",
    "medical_record_number": "MRN-20418",
    "full_name": "Ana García López",
    "email": null,
    "phone": "+14155550199",
    "date_of_birth": "1988-04-12",
    "sex": "female",
    "preferred_language": "es",
    "sms_opt_in": true,
    "archived_at": null,
    "created_at": "2026-09-01T15:04:05.123456+00:00",
    "updated_at": "2026-09-19T10:31:47.482913+00:00"
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
POST/patients/upsert

Request· Required fields: full_nameexternal_id

POST /api/v1/patients/upsert
Content-Type: application/json

{
  "external_id": "ERP-P-1042",
  "full_name": "Ana García López",
  "phone": "+14155550123"
}

Response 200 OK

{
  "data": {
    "id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
    "external_id": "ERP-P-1042",
    "created": false
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}

Response 201 Created (new external_id)

{
  "data": {
    "id": "5d0c8e37-a1b4-4f92-86c3-7e1a09b2d548",
    "external_id": "ERP-P-1043",
    "created": true
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
POST/patients/batch

Request· Required fields: items

POST /api/v1/patients/batch
Content-Type: application/json

{
  "items": [
    {
      "external_id": "ERP-P-1042",
      "full_name": "Ana García López",
      "phone": "+14155550123"
    },
    {
      "external_id": "ERP-P-1043",
      "full_name": "Luis Herrera"
    },
    {
      "external_id": "ERP-P-1044",
      "full_name": "Sofía Vega",
      "email": "not-an-email"
    }
  ]
}

Response 200 OK

{
  "data": {
    "results": [
      {
        "index": 0,
        "status": "updated",
        "id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23"
      },
      {
        "index": 1,
        "status": "created",
        "id": "5d0c8e37-a1b4-4f92-86c3-7e1a09b2d548"
      },
      {
        "index": 2,
        "status": "error",
        "id": null,
        "error": {
          "code": "VALIDATION_ERROR",
          "message": "Invalid email address",
          "field": "email"
        }
      }
    ],
    "summary": {
      "created": 1,
      "updated": 1,
      "failed": 1
    }
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
DELETE/patients/8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23

Request

DELETE /api/v1/patients/8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23

Response 200 OK (patient with history: archived, record retained)

{
  "data": {
    "id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
    "result": "archived",
    "deleted": false,
    "archived": true,
    "retain_until": "2036-10-06T15:02:11Z",
    "cancelled_appointments": 1
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}

Response 200 OK (patient with no history)

{
  "data": {
    "id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
    "result": "deleted",
    "deleted": true,
    "archived": false,
    "retain_until": null,
    "cancelled_appointments": 0
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
Appointments6
GET/appointments

Request

GET /api/v1/appointments?from=2026-10-05T00:00:00Z&to=2026-10-06T00:00:00Z&status=scheduled&expand=patient,service&limit=1

Response 200 OK

{
  "data": [
    {
      "id": "0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18",
      "external_id": "ERP-APPT-9001",
      "patient_id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
      "provider_id": "17ab90de-5c42-4e8f-a3d1-2b6f8c0e9d74",
      "service_id": "4e2a7b19-0d63-4c58-b8f4-9a1e3d5c7f60",
      "starts_at": "2026-10-05T14:00:00+00:00",
      "ends_at": "2026-10-05T14:45:00+00:00",
      "status": "scheduled",
      "notes": "Follow-up on lab results",
      "created_at": "2026-09-01T15:04:05.123456+00:00",
      "updated_at": "2026-09-19T10:31:47.482913+00:00",
      "patient": {
        "id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
        "external_id": "ERP-P-1042",
        "medical_record_number": "MRN-20418",
        "full_name": "Ana García López",
        "email": "ana.garcia@example.com",
        "phone": "+14155550123"
      },
      "service": {
        "id": "4e2a7b19-0d63-4c58-b8f4-9a1e3d5c7f60",
        "external_id": "ERP-SVC-12",
        "name": "Follow-up visit",
        "duration_minutes": 45,
        "price_cents": 8500
      }
    }
  ],
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05",
    "next_cursor": "eyJ2IjoxLCJzIjoic3RhcnRzX2F0IiwiZCI6ImFzYyIsImx2IjoiMjAyNi0xMC0wNVQxNDowMDowMCswMDowMCIsImxpIjoiMGMxZTVhOWEifQ.Zk3vP9",
    "has_more": true,
    "limit": 1
  }
}
POST/appointments

Request· Required fields: patient_idprovider_idstarts_atends_at

POST /api/v1/appointments
Idempotency-Key: erp-appt-9001-create
Content-Type: application/json

{
  "patient_id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
  "provider_id": "17ab90de-5c42-4e8f-a3d1-2b6f8c0e9d74",
  "service_id": "4e2a7b19-0d63-4c58-b8f4-9a1e3d5c7f60",
  "starts_at": "2026-10-05T14:00:00Z",
  "ends_at": "2026-10-05T14:45:00Z",
  "notes": "Follow-up on lab results",
  "external_id": "ERP-APPT-9001",
  "notify": false
}

Response 201 Created

{
  "data": {
    "id": "0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18",
    "external_id": "ERP-APPT-9001",
    "patient_id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
    "provider_id": "17ab90de-5c42-4e8f-a3d1-2b6f8c0e9d74",
    "service_id": "4e2a7b19-0d63-4c58-b8f4-9a1e3d5c7f60",
    "starts_at": "2026-10-05T14:00:00+00:00",
    "ends_at": "2026-10-05T14:45:00+00:00",
    "status": "scheduled",
    "notes": "Follow-up on lab results",
    "created_at": "2026-09-01T15:04:05.123456+00:00",
    "updated_at": "2026-09-19T10:31:47.482913+00:00"
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05",
    "created": true
  }
}

Response 409 Conflict (slot already taken)

{
  "data": null,
  "error": {
    "code": "CONFLICT",
    "message": "The provider already has an appointment at this time"
  },
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
PATCH/appointments/0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18

Request

PATCH /api/v1/appointments/0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18
If-Match: 2026-09-19T10:31:47.482913+00:00
Content-Type: application/json

{
  "starts_at": "2026-10-06T15:00:00Z",
  "ends_at": "2026-10-06T15:45:00Z"
}

Response 200 OK (reschedule)

{
  "data": {
    "id": "0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18",
    "external_id": "ERP-APPT-9001",
    "patient_id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
    "provider_id": "17ab90de-5c42-4e8f-a3d1-2b6f8c0e9d74",
    "service_id": "4e2a7b19-0d63-4c58-b8f4-9a1e3d5c7f60",
    "starts_at": "2026-10-06T15:00:00+00:00",
    "ends_at": "2026-10-06T15:45:00+00:00",
    "status": "scheduled",
    "notes": "Follow-up on lab results",
    "created_at": "2026-09-01T15:04:05.123456+00:00",
    "updated_at": "2026-09-19T11:02:13.907245+00:00"
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}

Response 412 Precondition Failed (changed since you read it)

{
  "data": null,
  "error": {
    "code": "PRECONDITION_FAILED",
    "message": "The appointment has changed since you read it; re-read it and retry",
    "details": {
      "current_updated_at": "2026-09-19T10:58:40.115602+00:00"
    }
  },
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
PATCH/appointments/0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18

Request

PATCH /api/v1/appointments/0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18
Content-Type: application/json

{
  "status": "completed"
}

Response 200 OK

{
  "data": {
    "id": "0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18",
    "external_id": "ERP-APPT-9001",
    "patient_id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
    "provider_id": "17ab90de-5c42-4e8f-a3d1-2b6f8c0e9d74",
    "service_id": "4e2a7b19-0d63-4c58-b8f4-9a1e3d5c7f60",
    "starts_at": "2026-10-05T14:00:00+00:00",
    "ends_at": "2026-10-05T14:45:00+00:00",
    "status": "completed",
    "notes": "Follow-up on lab results",
    "created_at": "2026-09-01T15:04:05.123456+00:00",
    "updated_at": "2026-09-19T10:31:47.482913+00:00"
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}

Response 409 Conflict (already finished)

{
  "data": null,
  "error": {
    "code": "INVALID_TRANSITION",
    "message": "Cannot change status from cancelled to completed",
    "details": {
      "from": "cancelled",
      "to": "completed"
    }
  },
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
POST/appointments/0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18/cancel

Request

POST /api/v1/appointments/0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18/cancel?notify=false

Response 200 OK

{
  "data": {
    "id": "0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18",
    "external_id": "ERP-APPT-9001",
    "patient_id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
    "provider_id": "17ab90de-5c42-4e8f-a3d1-2b6f8c0e9d74",
    "service_id": "4e2a7b19-0d63-4c58-b8f4-9a1e3d5c7f60",
    "starts_at": "2026-10-05T14:00:00+00:00",
    "ends_at": "2026-10-05T14:45:00+00:00",
    "status": "cancelled",
    "notes": "Follow-up on lab results",
    "created_at": "2026-09-01T15:04:05.123456+00:00",
    "updated_at": "2026-09-19T11:20:09.331870+00:00"
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
POST/appointments/batch

Request· Required fields: items

POST /api/v1/appointments/batch
Content-Type: application/json

{
  "items": [
    {
      "external_id": "ERP-APPT-9001",
      "status": "confirmed"
    },
    {
      "external_id": "ERP-APPT-9002",
      "patient_id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
      "provider_id": "17ab90de-5c42-4e8f-a3d1-2b6f8c0e9d74",
      "starts_at": "2026-10-07T16:00:00Z",
      "ends_at": "2026-10-07T16:30:00Z"
    },
    {
      "external_id": "ERP-APPT-9003",
      "patient_id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
      "provider_id": "17ab90de-5c42-4e8f-a3d1-2b6f8c0e9d74",
      "starts_at": "2026-10-07T16:15:00Z",
      "ends_at": "2026-10-07T16:45:00Z"
    }
  ]
}

Response 200 OK

{
  "data": {
    "results": [
      {
        "index": 0,
        "status": "updated",
        "id": "0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18"
      },
      {
        "index": 1,
        "status": "created",
        "id": "7f3b2a10-c8d4-4e59-a1b6-04d9e5c73f82"
      },
      {
        "index": 2,
        "status": "error",
        "id": null,
        "error": {
          "code": "CONFLICT",
          "message": "The provider already has an appointment at this time"
        }
      }
    ],
    "summary": {
      "created": 1,
      "updated": 1,
      "failed": 1
    }
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
Services3
POST/services

Request· Required fields: name

POST /api/v1/services
Idempotency-Key: erp-svc-12-create
Content-Type: application/json

{
  "name": "Follow-up visit",
  "description": "Review of results and treatment plan",
  "duration_minutes": 45,
  "buffer_minutes": 10,
  "color": "#6366f1",
  "price_cents": 8500,
  "external_id": "ERP-SVC-12"
}

Response 201 Created

{
  "data": {
    "id": "4e2a7b19-0d63-4c58-b8f4-9a1e3d5c7f60",
    "external_id": "ERP-SVC-12",
    "name": "Follow-up visit",
    "description": "Review of results and treatment plan",
    "duration_minutes": 45,
    "buffer_minutes": 10,
    "color": "#6366f1",
    "price_cents": 8500,
    "created_at": "2026-09-01T15:04:05.123456+00:00",
    "updated_at": "2026-09-19T10:31:47.482913+00:00"
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05",
    "created": true
  }
}
PATCH/services/4e2a7b19-0d63-4c58-b8f4-9a1e3d5c7f60

Request

PATCH /api/v1/services/4e2a7b19-0d63-4c58-b8f4-9a1e3d5c7f60
Content-Type: application/json

{
  "price_cents": 9000,
  "description": null
}

Response 200 OK

{
  "data": {
    "id": "4e2a7b19-0d63-4c58-b8f4-9a1e3d5c7f60",
    "external_id": "ERP-SVC-12",
    "name": "Follow-up visit",
    "description": null,
    "duration_minutes": 45,
    "buffer_minutes": 10,
    "color": "#6366f1",
    "price_cents": 9000,
    "created_at": "2026-09-01T15:04:05.123456+00:00",
    "updated_at": "2026-09-19T10:31:47.482913+00:00"
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
DELETE/services/4e2a7b19-0d63-4c58-b8f4-9a1e3d5c7f60

Request

DELETE /api/v1/services/4e2a7b19-0d63-4c58-b8f4-9a1e3d5c7f60?force=true

Response 200 OK

{
  "data": {
    "id": "4e2a7b19-0d63-4c58-b8f4-9a1e3d5c7f60",
    "deleted": true
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
Providers3
GET/providers/17ab90de-5c42-4e8f-a3d1-2b6f8c0e9d74

Request

GET /api/v1/providers/17ab90de-5c42-4e8f-a3d1-2b6f8c0e9d74

Response 200 OK

{
  "data": {
    "id": "17ab90de-5c42-4e8f-a3d1-2b6f8c0e9d74",
    "external_id": "ERP-DR-7",
    "full_name": "Dr. Elena Ruiz",
    "specialty": "Physiotherapy",
    "bio": "Sports injuries and post-surgical rehabilitation.",
    "timezone": "America/Mexico_City",
    "availability": [
      {
        "day": 1,
        "start": "09:00",
        "end": "17:00"
      },
      {
        "day": 3,
        "start": "09:00",
        "end": "17:00"
      }
    ],
    "max_appointments_per_day": 12,
    "booking_window_days": 60,
    "portal_restricted_days": [
      0,
      6
    ],
    "cedula_profesional": "1234567",
    "deactivated_at": null,
    "created_at": "2026-09-01T15:04:05.123456+00:00",
    "updated_at": "2026-09-19T10:31:47.482913+00:00"
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
PATCH/providers/17ab90de-5c42-4e8f-a3d1-2b6f8c0e9d74

Request

PATCH /api/v1/providers/17ab90de-5c42-4e8f-a3d1-2b6f8c0e9d74
Content-Type: application/json

{
  "availability": [
    {
      "day": 1,
      "start": "09:00",
      "end": "13:00"
    },
    {
      "day": 1,
      "start": "14:00",
      "end": "18:00"
    }
  ],
  "availability_overrides": [
    {
      "date": "2026-12-24",
      "available": false
    }
  ],
  "max_appointments_per_day": 10,
  "booking_window_days": null,
  "external_id": "ERP-DR-7"
}

Response 200 OK

{
  "data": {
    "id": "17ab90de-5c42-4e8f-a3d1-2b6f8c0e9d74",
    "external_id": "ERP-DR-7",
    "full_name": "Dr. Elena Ruiz",
    "specialty": "Physiotherapy",
    "bio": "Sports injuries and post-surgical rehabilitation.",
    "timezone": "America/Mexico_City",
    "availability": [
      {
        "day": 1,
        "start": "09:00",
        "end": "13:00"
      },
      {
        "day": 1,
        "start": "14:00",
        "end": "18:00"
      }
    ],
    "max_appointments_per_day": 10,
    "booking_window_days": null,
    "portal_restricted_days": [
      0,
      6
    ],
    "cedula_profesional": "1234567",
    "deactivated_at": null,
    "created_at": "2026-09-01T15:04:05.123456+00:00",
    "updated_at": "2026-09-19T10:31:47.482913+00:00"
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
GET/providers/17ab90de-5c42-4e8f-a3d1-2b6f8c0e9d74/availability

Request

GET /api/v1/providers/17ab90de-5c42-4e8f-a3d1-2b6f8c0e9d74/availability?from=2026-10-05T00:00:00Z&to=2026-10-06T00:00:00Z&service_id=4e2a7b19-0d63-4c58-b8f4-9a1e3d5c7f60

Response 200 OK

{
  "data": {
    "provider_id": "17ab90de-5c42-4e8f-a3d1-2b6f8c0e9d74",
    "timezone": "America/Mexico_City",
    "duration_minutes": 45,
    "buffer_minutes": 10,
    "slots": [
      {
        "starts_at": "2026-10-05T15:00:00.000Z",
        "ends_at": "2026-10-05T15:45:00.000Z"
      },
      {
        "starts_at": "2026-10-05T15:55:00.000Z",
        "ends_at": "2026-10-05T16:40:00.000Z"
      }
    ]
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
Clinical records (Network)8
GET/patients/8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23/record

Request

GET /api/v1/patients/8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23/record?limit=1

Response 200 OK

{
  "data": [
    {
      "appointment": {
        "id": "0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18",
        "starts_at": "2026-10-05T14:00:00+00:00",
        "ends_at": "2026-10-05T14:45:00+00:00",
        "status": "completed",
        "notes": null
      },
      "clinical_notes": [
        {
          "id": "b3f81d64-8a27-4e05-9c3b-71d2f4a6e589",
          "appointment_id": "0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18",
          "patient_id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
          "author_name": "Dr. Elena Ruiz",
          "note_type": "soap",
          "is_draft": false,
          "status": "signed",
          "parent_note_id": null,
          "subjective": "Reports less pain when climbing stairs.",
          "objective": "Range of motion improved to 110°.",
          "assessment": "Recovering as expected.",
          "plan_text": "Continue exercises; review in two weeks.",
          "content": null,
          "vitals": {
            "bp_systolic": {
              "value": 124,
              "unit": "mm[Hg]",
              "code": "8480-6"
            },
            "bp_diastolic": {
              "value": 79,
              "unit": "mm[Hg]",
              "code": "8462-4"
            },
            "heart_rate": {
              "value": 68,
              "unit": "/min",
              "code": "8867-4"
            },
            "weight": {
              "value": 64.5,
              "unit": "kg",
              "code": "29463-7"
            },
            "height": {
              "value": 168,
              "unit": "cm",
              "code": "8302-2"
            },
            "bmi": {
              "value": 22.9,
              "unit": "kg/m2",
              "code": "39156-5"
            }
          },
          "template_key": null,
          "template_data": null,
          "signed_by_name": "Dr. Elena Ruiz",
          "signed_by_credential": "1234567",
          "signed_at": "2026-09-19T10:31:47.482913+00:00",
          "attestation": "I attest that this entry is accurate and complete and that I wrote it. Once signed it cannot be changed; any correction must be made with an addendum.",
          "legacy_signature": false,
          "voided_by_name": null,
          "voided_at": null,
          "void_reason": null,
          "created_at": "2026-09-01T15:04:05.123456+00:00",
          "updated_at": "2026-09-19T10:31:47.482913+00:00"
        }
      ],
      "intake": {
        "id": "c7a41e92-3d58-4b06-8f1a-9e2d60b5c3f7",
        "appointment_id": "0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18",
        "patient_id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
        "status": "submitted",
        "reason_for_visit": "Knee pain after running",
        "chief_complaint": "Pain on the inner side of the right knee",
        "symptom_onset": "3 weeks ago",
        "symptom_description": "Sharp when descending stairs",
        "pain_scale": 6,
        "recent_changes": "Started a half-marathon training plan",
        "current_medications": "Ibuprofen 400 mg as needed",
        "known_allergies": "Penicillin",
        "relevant_conditions": null,
        "address_confirmed": true,
        "emergency_contact_confirmed": true,
        "insurance_confirmed": false,
        "submitted_at": "2026-09-01T15:04:05.123456+00:00",
        "reviewed_at": null,
        "review_notes": null,
        "created_at": "2026-09-01T15:04:05.123456+00:00",
        "updated_at": "2026-09-01T15:04:05.123456+00:00"
      }
    }
  ],
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
GET/patients/8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23/documents

Request

GET /api/v1/patients/8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23/documents

Response 200 OK

{
  "data": [
    {
      "id": "6a2d9f41-0b3c-4e78-a5d6-1f8e2b7c9d03",
      "patient_id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
      "appointment_id": "0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18",
      "category": "imaging",
      "title": "MRI right knee",
      "mime_type": "application/pdf",
      "size_bytes": 482113,
      "sha256": "9f2c4e81b7a05d36e1c8f40b2a97d6e53c0b18f7a4e2d95c6b3f0a8e71d4c2b9",
      "uploaded_by_name": "Dr. Elena Ruiz",
      "created_at": "2026-09-19T10:31:47.482913+00:00",
      "voided_at": null,
      "voided_by_name": null,
      "void_reason": null
    }
  ],
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
GET/patients/8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23/documents/6a2d9f41-0b3c-4e78-a5d6-1f8e2b7c9d03

Request

GET /api/v1/patients/8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23/documents/6a2d9f41-0b3c-4e78-a5d6-1f8e2b7c9d03

Response 200 OK (the url expires after 60 seconds)

{
  "data": {
    "id": "6a2d9f41-0b3c-4e78-a5d6-1f8e2b7c9d03",
    "patient_id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
    "appointment_id": "0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18",
    "category": "imaging",
    "title": "MRI right knee",
    "mime_type": "application/pdf",
    "size_bytes": 482113,
    "sha256": "9f2c4e81b7a05d36e1c8f40b2a97d6e53c0b18f7a4e2d95c6b3f0a8e71d4c2b9",
    "uploaded_by_name": "Dr. Elena Ruiz",
    "created_at": "2026-09-19T10:31:47.482913+00:00",
    "voided_at": null,
    "voided_by_name": null,
    "void_reason": null,
    "url": "https://…/storage/v1/object/sign/patient-documents/…?token=…",
    "url_expires_at": "2026-09-19T10:32:47.000Z"
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
POST/appointments/0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18/notes

Request

POST /api/v1/appointments/0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18/notes
Idempotency-Key: erp-note-5521
Content-Type: application/json

{
  "note_type": "soap",
  "subjective": "Reports less pain when climbing stairs.",
  "objective": "Range of motion improved to 110°.",
  "assessment": "Recovering as expected.",
  "plan_text": "Continue exercises; review in two weeks.",
  "vitals": {
    "bp_systolic": 124,
    "bp_diastolic": 79,
    "heart_rate": 68,
    "weight": {
      "value": 64.5,
      "unit": "kg"
    },
    "height": 168
  }
}

Response 201 Created (a draft for the clinician to sign)

{
  "data": {
    "id": "b3f81d64-8a27-4e05-9c3b-71d2f4a6e589",
    "appointment_id": "0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18",
    "patient_id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
    "author_name": "Dr. Elena Ruiz",
    "note_type": "soap",
    "is_draft": true,
    "status": "draft",
    "parent_note_id": null,
    "subjective": "Reports less pain when climbing stairs.",
    "objective": "Range of motion improved to 110°.",
    "assessment": "Recovering as expected.",
    "plan_text": "Continue exercises; review in two weeks.",
    "content": null,
    "vitals": {
      "bp_systolic": {
        "value": 124,
        "unit": "mm[Hg]",
        "code": "8480-6"
      },
      "bp_diastolic": {
        "value": 79,
        "unit": "mm[Hg]",
        "code": "8462-4"
      },
      "heart_rate": {
        "value": 68,
        "unit": "/min",
        "code": "8867-4"
      },
      "weight": {
        "value": 64.5,
        "unit": "kg",
        "code": "29463-7"
      },
      "height": {
        "value": 168,
        "unit": "cm",
        "code": "8302-2"
      },
      "bmi": {
        "value": 22.9,
        "unit": "kg/m2",
        "code": "39156-5"
      }
    },
    "template_key": null,
    "template_data": null,
    "signed_by_name": null,
    "signed_by_credential": null,
    "signed_at": null,
    "attestation": null,
    "legacy_signature": false,
    "voided_by_name": null,
    "voided_at": null,
    "void_reason": null,
    "created_at": "2026-09-01T15:04:05.123456+00:00",
    "updated_at": "2026-09-01T15:04:05.123456+00:00"
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}

Response 403 Forbidden (is_draft: false)

{
  "data": null,
  "error": {
    "code": "NOTE_SIGN_REQUIRES_CLINICIAN",
    "message": "API keys create drafts only; a clinician signs the note in the dashboard"
  },
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
PATCH/appointments/0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18/notes/b3f81d64-8a27-4e05-9c3b-71d2f4a6e589

Request

PATCH /api/v1/appointments/0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18/notes/b3f81d64-8a27-4e05-9c3b-71d2f4a6e589
Content-Type: application/json

{
  "plan_text": "Continue exercises; review in three weeks."
}

Response 200 OK (still a draft)

{
  "data": {
    "id": "b3f81d64-8a27-4e05-9c3b-71d2f4a6e589",
    "appointment_id": "0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18",
    "patient_id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
    "author_name": "Dr. Elena Ruiz",
    "note_type": "soap",
    "is_draft": true,
    "status": "draft",
    "parent_note_id": null,
    "subjective": "Reports less pain when climbing stairs.",
    "objective": "Range of motion improved to 110°.",
    "assessment": "Recovering as expected.",
    "plan_text": "Continue exercises; review in three weeks.",
    "content": null,
    "vitals": {
      "bp_systolic": {
        "value": 124,
        "unit": "mm[Hg]",
        "code": "8480-6"
      },
      "bp_diastolic": {
        "value": 79,
        "unit": "mm[Hg]",
        "code": "8462-4"
      },
      "heart_rate": {
        "value": 68,
        "unit": "/min",
        "code": "8867-4"
      },
      "weight": {
        "value": 64.5,
        "unit": "kg",
        "code": "29463-7"
      },
      "height": {
        "value": 168,
        "unit": "cm",
        "code": "8302-2"
      },
      "bmi": {
        "value": 22.9,
        "unit": "kg/m2",
        "code": "39156-5"
      }
    },
    "template_key": null,
    "template_data": null,
    "signed_by_name": null,
    "signed_by_credential": null,
    "signed_at": null,
    "attestation": null,
    "legacy_signature": false,
    "voided_by_name": null,
    "voided_at": null,
    "void_reason": null,
    "created_at": "2026-09-01T15:04:05.123456+00:00",
    "updated_at": "2026-09-01T15:04:05.123456+00:00"
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}

Response 409 Conflict (already signed)

{
  "data": null,
  "error": {
    "code": "NOTE_LOCKED",
    "message": "This note is signed and cannot be changed or deleted; add an addendum instead"
  },
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
POST/appointments/0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18/notes

Request

POST /api/v1/appointments/0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18/notes
Idempotency-Key: erp-note-5530
Content-Type: application/json

{
  "template_key": "mental-health.phq9@1",
  "template_data": {
    "phq9": {
      "answers": [
        1,
        2,
        1,
        1,
        2,
        1,
        1,
        1,
        0
      ],
      "difficulty": 1
    },
    "comments": "Sleep is the main complaint."
  }
}

Response 201 Created (a draft; total and band computed by Genkō)

{
  "data": {
    "id": "5c8e1a27-94d3-4b06-8f21-7e3a9d0c6b48",
    "appointment_id": "0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18",
    "patient_id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
    "author_name": "Dr. Elena Ruiz",
    "note_type": "progress",
    "is_draft": true,
    "status": "draft",
    "parent_note_id": null,
    "subjective": null,
    "objective": null,
    "assessment": null,
    "plan_text": null,
    "content": null,
    "vitals": null,
    "template_key": "mental-health.phq9@1",
    "template_data": {
      "phq9": {
        "answers": [
          1,
          2,
          1,
          1,
          2,
          1,
          1,
          1,
          0
        ],
        "difficulty": 1,
        "total": 10,
        "severity": "moderate"
      },
      "comments": "Sleep is the main complaint."
    },
    "signed_by_name": null,
    "signed_by_credential": null,
    "signed_at": null,
    "attestation": null,
    "legacy_signature": false,
    "voided_by_name": null,
    "voided_at": null,
    "void_reason": null,
    "created_at": "2026-09-01T15:04:05.123456+00:00",
    "updated_at": "2026-09-01T15:04:05.123456+00:00"
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}

Response 400 Bad Request (a partial questionnaire)

{
  "data": null,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "mental-health.phq9@1: phq9.answers: PHQ-9 needs all 9 answers (0–3)",
    "details": {
      "field": "template_data.phq9.answers"
    }
  },
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
POST/appointments/0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18/notes/b3f81d64-8a27-4e05-9c3b-71d2f4a6e589/addenda

Request· Required fields: content

POST /api/v1/appointments/0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18/notes/b3f81d64-8a27-4e05-9c3b-71d2f4a6e589/addenda
Idempotency-Key: erp-addendum-5522
Content-Type: application/json

{
  "content": "MRI report received: no meniscal tear."
}

Response 201 Created (a draft addendum)

{
  "data": {
    "id": "e3b9c2d4-7a15-4f68-9b0e-2c4d6f8a1b35",
    "appointment_id": "0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18",
    "patient_id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
    "author_name": "Dr. Elena Ruiz",
    "note_type": "addendum",
    "is_draft": true,
    "status": "draft",
    "parent_note_id": "b3f81d64-8a27-4e05-9c3b-71d2f4a6e589",
    "subjective": null,
    "objective": null,
    "assessment": null,
    "plan_text": null,
    "content": "MRI report received: no meniscal tear.",
    "vitals": {
      "bp_systolic": {
        "value": 124,
        "unit": "mm[Hg]",
        "code": "8480-6"
      },
      "bp_diastolic": {
        "value": 79,
        "unit": "mm[Hg]",
        "code": "8462-4"
      },
      "heart_rate": {
        "value": 68,
        "unit": "/min",
        "code": "8867-4"
      },
      "weight": {
        "value": 64.5,
        "unit": "kg",
        "code": "29463-7"
      },
      "height": {
        "value": 168,
        "unit": "cm",
        "code": "8302-2"
      },
      "bmi": {
        "value": 22.9,
        "unit": "kg/m2",
        "code": "39156-5"
      }
    },
    "template_key": null,
    "template_data": null,
    "signed_by_name": null,
    "signed_by_credential": null,
    "signed_at": null,
    "attestation": null,
    "legacy_signature": false,
    "voided_by_name": null,
    "voided_at": null,
    "void_reason": null,
    "created_at": "2026-09-01T15:04:05.123456+00:00",
    "updated_at": "2026-09-01T15:04:05.123456+00:00"
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
POST/appointments/0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18/intake/review

Request· Required fields: status

POST /api/v1/appointments/0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18/intake/review
Content-Type: application/json

{
  "status": "approved",
  "review_notes": "Insurance to be confirmed at reception."
}

Response 200 OK

{
  "data": {
    "id": "c7a41e92-3d58-4b06-8f1a-9e2d60b5c3f7",
    "appointment_id": "0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18",
    "patient_id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
    "status": "approved",
    "reason_for_visit": "Knee pain after running",
    "chief_complaint": "Pain on the inner side of the right knee",
    "symptom_onset": "3 weeks ago",
    "symptom_description": "Sharp when descending stairs",
    "pain_scale": 6,
    "recent_changes": "Started a half-marathon training plan",
    "current_medications": "Ibuprofen 400 mg as needed",
    "known_allergies": "Penicillin",
    "relevant_conditions": null,
    "address_confirmed": true,
    "emergency_contact_confirmed": true,
    "insurance_confirmed": false,
    "submitted_at": "2026-09-01T15:04:05.123456+00:00",
    "reviewed_at": "2026-09-19T10:31:47.482913+00:00",
    "review_notes": "Insurance to be confirmed at reception.",
    "created_at": "2026-09-01T15:04:05.123456+00:00",
    "updated_at": "2026-09-01T15:04:05.123456+00:00"
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
Webhooks (Network)6
POST/webhooks

Request· Required fields: urlevents

POST /api/v1/webhooks
Content-Type: application/json

{
  "url": "https://erp.example.com/genko-webhook",
  "description": "ERP sync",
  "events": [
    "appointment.created",
    "appointment.updated",
    "appointment.cancelled",
    "patient.updated"
  ]
}

Response 201 Created

{
  "data": {
    "id": "e91c3a5d-40b7-4f82-a6d3-58c1b9e07f24",
    "url": "https://erp.example.com/genko-webhook",
    "description": "ERP sync",
    "events": [
      "appointment.created",
      "appointment.updated",
      "appointment.cancelled",
      "patient.updated"
    ],
    "active": true,
    "disabled_reason": null,
    "disabled_at": null,
    "consecutive_failures": 0,
    "previous_secret_expires_at": null,
    "created_at": "2026-09-01T15:04:05.123456+00:00",
    "updated_at": "2026-09-01T15:04:05.123456+00:00",
    "secret": "whsec_4f9c1e7a2b8d3065a1c7e94d0b2f8a61c3d5e7f9"
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
PATCH/webhooks/e91c3a5d-40b7-4f82-a6d3-58c1b9e07f24

Request

PATCH /api/v1/webhooks/e91c3a5d-40b7-4f82-a6d3-58c1b9e07f24
Content-Type: application/json

{
  "events": [
    "appointment.created",
    "appointment.cancelled"
  ],
  "active": true
}

Response 200 OK

{
  "data": {
    "id": "e91c3a5d-40b7-4f82-a6d3-58c1b9e07f24",
    "url": "https://erp.example.com/genko-webhook",
    "description": "ERP sync",
    "events": [
      "appointment.created",
      "appointment.cancelled"
    ],
    "active": true,
    "disabled_reason": null,
    "disabled_at": null,
    "consecutive_failures": 0,
    "previous_secret_expires_at": null,
    "created_at": "2026-09-01T15:04:05.123456+00:00",
    "updated_at": "2026-09-01T15:04:05.123456+00:00"
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
POST/webhooks/e91c3a5d-40b7-4f82-a6d3-58c1b9e07f24/rotate-secret

Request

POST /api/v1/webhooks/e91c3a5d-40b7-4f82-a6d3-58c1b9e07f24/rotate-secret?grace_hours=48

Response 200 OK

{
  "data": {
    "id": "e91c3a5d-40b7-4f82-a6d3-58c1b9e07f24",
    "url": "https://erp.example.com/genko-webhook",
    "description": "ERP sync",
    "events": [
      "appointment.created",
      "appointment.updated",
      "appointment.cancelled",
      "patient.updated"
    ],
    "active": true,
    "disabled_reason": null,
    "disabled_at": null,
    "consecutive_failures": 0,
    "previous_secret_expires_at": "2026-09-21T11:02:13.907245+00:00",
    "created_at": "2026-09-01T15:04:05.123456+00:00",
    "updated_at": "2026-09-01T15:04:05.123456+00:00",
    "secret": "whsec_b7d20e4f91a3c58e6d0f2b1a97c4e3850d6f1a28"
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
POST/webhooks/e91c3a5d-40b7-4f82-a6d3-58c1b9e07f24/test

Request

POST /api/v1/webhooks/e91c3a5d-40b7-4f82-a6d3-58c1b9e07f24/test

Response 200 OK

{
  "data": {
    "event_id": "1e8a4c70-5b93-4d2f-a0c6-93f7b2d8e415",
    "delivered": true,
    "status_code": 200,
    "error": null,
    "duration_ms": 184
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}

Response 200 OK (receiver unreachable)

{
  "data": {
    "event_id": "1e8a4c70-5b93-4d2f-a0c6-93f7b2d8e415",
    "delivered": false,
    "status_code": null,
    "error": "timeout",
    "duration_ms": 10001
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
GET/webhooks/e91c3a5d-40b7-4f82-a6d3-58c1b9e07f24/deliveries

Request

GET /api/v1/webhooks/e91c3a5d-40b7-4f82-a6d3-58c1b9e07f24/deliveries?status=failed&limit=1

Response 200 OK

{
  "data": [
    {
      "id": "5b0f7a3e-9c2d-4f6e-8a51-0d3c7e1b2a44",
      "endpoint_id": "e91c3a5d-40b7-4f82-a6d3-58c1b9e07f24",
      "event_type": "appointment.updated",
      "resource_type": "appointment",
      "resource_id": "0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18",
      "payload": {
        "id": "5b0f7a3e-9c2d-4f6e-8a51-0d3c7e1b2a44",
        "type": "appointment.updated",
        "created_at": "2026-09-19T11:02:13.907245Z",
        "data": {
          "id": "0c1e5a9a-72b4-4d3f-8e61-a5f90b2c4d18",
          "external_id": "ERP-APPT-9001",
          "patient_id": "8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23",
          "provider_id": "17ab90de-5c42-4e8f-a3d1-2b6f8c0e9d74",
          "changed_fields": [
            "ends_at",
            "starts_at"
          ]
        }
      },
      "status": "failed",
      "attempts": 2,
      "next_attempt_at": "2026-09-19T11:37:14.000000+00:00",
      "last_attempt_at": "2026-09-19T11:07:14.000000+00:00",
      "last_status_code": 500,
      "last_error": "HTTP 500",
      "delivered_at": null,
      "created_at": "2026-09-19T11:02:13.907245+00:00"
    }
  ],
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05",
    "next_cursor": null,
    "has_more": false,
    "limit": 1
  }
}
POST/webhooks/e91c3a5d-40b7-4f82-a6d3-58c1b9e07f24/deliveries/5b0f7a3e-9c2d-4f6e-8a51-0d3c7e1b2a44/redeliver

Request

POST /api/v1/webhooks/e91c3a5d-40b7-4f82-a6d3-58c1b9e07f24/deliveries/5b0f7a3e-9c2d-4f6e-8a51-0d3c7e1b2a44/redeliver

Response 200 OK

{
  "data": {
    "id": "5b0f7a3e-9c2d-4f6e-8a51-0d3c7e1b2a44",
    "status": "pending"
  },
  "error": null,
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
Errors6
POST/patients

Request

POST /api/v1/patients
Content-Type: application/json

{
  "full_name": "Ana García",
  "email": "ana.garcia",
  "nickname": "Ani"
}

Response 400 Bad Request

{
  "data": null,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid body",
    "details": {
      "issues": [
        {
          "path": "email",
          "message": "Invalid email address"
        },
        {
          "path": "",
          "message": "Unrecognized key: \"nickname\""
        }
      ]
    }
  },
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
GET/patients/8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23

Request

GET /api/v1/patients/8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23
Authorization: Bearer genko_mcp_… (an MCP key)

Response 401 Unauthorized

{
  "data": null,
  "error": {
    "code": "UNAUTHORIZED",
    "message": "This is an MCP key, which only works on /api/mcp. Create a REST API key under Settings → Developers → REST API keys.",
    "details": {
      "key_kind": "mcp"
    }
  },
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
POST/patients

Request

POST /api/v1/patients
Authorization: Bearer genko_… (a read-only key)
Content-Type: application/json

{
  "full_name": "Ana García"
}

Response 403 Forbidden

{
  "data": null,
  "error": {
    "code": "FORBIDDEN",
    "message": "This API key does not have the required scope",
    "details": {
      "required_scope": "patients:write"
    }
  },
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
GET/patients/8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23/record

Request

GET /api/v1/patients/8d4f2c1a-3b7e-4a90-9f12-6c5d0e7a1b23/record
Authorization: Bearer genko_… (a Practice plan key)

Response 403 Forbidden

{
  "data": null,
  "error": {
    "code": "PLAN_LIMIT",
    "message": "This endpoint requires the enterprise plan or above",
    "details": {
      "required_plan": "enterprise"
    }
  },
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
GET/patients/2b9e6d40-0000-4000-8000-000000000000

Request

GET /api/v1/patients/2b9e6d40-0000-4000-8000-000000000000

Response 404 Not Found

{
  "data": null,
  "error": {
    "code": "NOT_FOUND",
    "message": "Patient not found"
  },
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}
GET/patients

Request

GET /api/v1/patients?limit=500

Response 429 Too Many Requests

Retry-After: 23
RateLimit-Limit: 600
RateLimit-Remaining: 0
RateLimit-Reset: 23

{
  "data": null,
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded",
    "details": {
      "limit": 600,
      "retry_after_seconds": 23
    }
  },
  "meta": {
    "request_id": "3f6c0b9e-1d24-4a7e-9b58-c2e41f7a6d05"
  }
}

Webhooks

Instead of polling, your system can be told when something changes. Add an endpoint under Settings → Developers → Webhooks (or with POST /webhooks) and Genkō sends it a signed HTTPS request whenever an appointment or a patient changes, whoever made the change: your own API calls, the dashboard, the patient portal, an import. Webhooks are a Network feature, with up to 10 endpoints per organization.

  • Events: appointment.created, appointment.updated, appointment.cancelled, appointment.completed, appointment.deleted, patient.created, patient.updated, patient.deleted, patient.archived, patient.restored. Each endpoint chooses which it receives. A change to cancelled or completed sends only that event, not appointment.updated as well.
  • No patient data in transit. An event carries ids and the names of the fields that changed, never their values. Read the record back with GET /patients/:id or GET /appointments/:id.
  • Independent of notify. notify: false stops emails, SMS and calendar sync; it does not stop webhooks, because they describe the change itself. Your own API writes therefore come back to you as events too: recognise them by external_id or by comparing with what you just wrote.
  • Consistent. Events are recorded in the same database transaction as the change, so none is lost after a commit and none is sent for a change that was rolled back.

What your server receives (a POST with a JSON body) and the headers that come with it:

{
  "id": "5b0f7a3e-9c2d-4f6e-8a51-0d3c7e1b2a44",
  "type": "appointment.updated",
  "created_at": "2026-10-09T12:00:00.123456Z",
  "data": {
    "id": "0c1e5a9a-…",
    "external_id": "ERP-APPT-9001",
    "patient_id": "8d4f…",
    "provider_id": "17ab…",
    "changed_fields": ["ends_at", "starts_at"]
  }
}
POST /genko-webhook HTTP/1.1
Content-Type: application/json
User-Agent: Genko-Webhooks/1
Genko-Signature: t=1790000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Genko-Event-Id: 5b0f7a3e-9c2d-4f6e-8a51-0d3c7e1b2a44
Genko-Event-Type: appointment.updated
Genko-Delivery-Attempt: 1

Verify every request

Each request carries Genko-Signature: t=…,v1=…. Compute an HMAC-SHA256 of the string t + "." + raw request body with your endpoint's secret (whsec_…, shown once when you create it) and compare it with v1 in constant time. Reject the request if t is more than 5 minutes from your clock, which stops a captured request being replayed. Always verify the raw body, before any JSON parsing. While a secret is being rotated the header carries two v1 values: accept either.

Node.js (Express)

import crypto from 'node:crypto'
import express from 'express'

const SECRETS = [process.env.GENKO_WEBHOOK_SECRET] // add the old secret too while you are rotating

// The signature covers the RAW bytes: verify before you parse any JSON.
function verify(header, rawBody) {
  const items = header.split(',').map((p) => p.split('='))
  const t = Number(items.find(([k]) => k === 't')?.[1])
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > 300) return false // replay window: 5 minutes
  const signatures = items.filter(([k]) => k === 'v1').map(([, v]) => v)
  return SECRETS.some((secret) => {
    const expected = crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex')
    return signatures.some((s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)))
  })
}

const app = express()
app.post('/genko-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const raw = req.body.toString('utf8')
  if (!verify(req.get('Genko-Signature') ?? '', raw)) return res.sendStatus(400)
  const event = JSON.parse(raw)
  // Dedupe on event.id (also the Genko-Event-Id header): delivery is at-least-once.
  res.sendStatus(200) // acknowledge within 10 seconds, do the real work afterwards
})

Python (Flask)

import hashlib, hmac, os, time
from flask import Flask, abort, request

app = Flask(__name__)
SECRETS = [os.environ["GENKO_WEBHOOK_SECRET"]]  # add the old secret too while you are rotating

def verify(header: str, raw_body: bytes) -> bool:
    items = [p.split("=", 1) for p in header.split(",") if "=" in p]
    t = next((v for k, v in items if k == "t"), "")
    if not t.isdigit() or abs(time.time() - int(t)) > 300:  # replay window: 5 minutes
        return False
    signatures = [v for k, v in items if k == "v1"]
    for secret in SECRETS:
        expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
        if any(hmac.compare_digest(s, expected) for s in signatures):
            return True
    return False

@app.post("/genko-webhook")
def genko_webhook():
    # The signature covers the RAW bytes: verify before you parse any JSON.
    if not verify(request.headers.get("Genko-Signature", ""), request.get_data()):
        abort(400)
    event = request.get_json()
    # Dedupe on event["id"] (also the Genko-Event-Id header): delivery is at-least-once.
    return "", 200  # acknowledge within 10 seconds, do the real work afterwards

Delivery, retries and health

  • At least once, not ordered. Dedupe on the event id (also sent as Genko-Event-Id; it never changes across retries), and use the record's updated_at to decide which version is newer.
  • Acknowledge with any 2xx within 10 seconds and do the real work afterwards. Anything else, including a redirect, a timeout or a connection error, counts as a failure and is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, then every 24 hours, up to 8 attempts. After that the delivery is marked gave up and stays available for 14 days to redeliver.
  • Retries run when they are due and the queue is next processed. While your integration is calling the API that is within about a minute; otherwise at the next scheduled sweep, which is at most a day away. Do not depend on a retry arriving at a precise time: updated_since polling remains the safety net.
  • Health. An endpoint that keeps failing for more than 24 hours (20 or more failed attempts in a row) is switched off and the organization owner is emailed. Events keep queueing for 14 days: fix the receiver, re-enable the endpoint and redeliver what you missed. One success resets the count.
  • Safe by design. Only public https:// addresses are accepted. Genkō resolves the host on every delivery and refuses private, loopback and link-local addresses, never follows redirects, and never stores your server's response.

Rotating the secret

Use Rotate secret in the dashboard or POST /webhooks/:id/rotate-secret. The new secret is shown once. For the grace period you choose (24 hours by default, up to 7 days, or none) deliveries are signed with both secrets, so you can deploy the new one without dropping an event; when it ends the old secret stops working. Lost a secret? Rotate it. Send test delivers a signed webhook.test event and shows your server's answer.

ERP integration guide

A typical integration is a one-off backfill, then near-real-time updates with a safety net. In order:

  1. Create a key. A Read & write REST API key covers scheduling and patient sync; add Admin only if you need clinical records. Restrict it to your server's IP addresses and give it an expiry you will remember to rotate.
  2. Agree on identifiers. Send your own id as external_id on every create and upsert. From then on you can look records up by it, re-send a create safely, and match webhook events to your rows.
  3. Backfill. Page GET /patients?limit=500 and GET /appointments?limit=500 to the end, and store the highest updated_at you saw as your watermark.
  4. Push changes in. Use POST /patients/upsert and POST /appointments (or the batch endpoints on Network) with an Idempotency-Key per change so a retry never doubles it, and notify: false if your system already messages patients.
  5. Pull changes out. Subscribe to the webhook events for near-real-time updates and fetch the record each one names. Keep a light ?updated_since=<watermark minus 60 s> poll as the safety net, since a webhook can be late but never silently lost from the queue. Deletes arrive as *.deleted events carrying your external_id.
  6. Reconcile. Once a week, page ?fields=id&limit=500 and compare ids with your copy to catch anything that went wrong on either side.
  7. Handle failures. On 429 wait Retry-After. On 5xx and 503 retry with exponential backoff and the same Idempotency-Key. Do not retry 400, 403 or 404. On 409 CONFLICT when booking, offer another slot from GET /providers/:id/availability.

Before you go live

  • Keys are stored as secrets and restricted by IP; nothing is sent in a URL.
  • Webhook receiver verifies the signature on the raw body, rejects old timestamps and dedupes on the event id.
  • The sync loop overlaps its watermark by 60 seconds and upserts by id or external_id.
  • Writes send an Idempotency-Key and back off on 429 and 5xx.
  • You have run Send test and one real change end to end, and know where deliveries and errors show up (Settings → Developers → Webhooks, and the audit log).
Machine-readable reference: the full endpoint, schema and error catalogue is at /api/v1/openapi.json (OpenAPI 3.1).

Calendar embeds

Overview

A calendar embed shows a practice's appointments — day, week, month or agenda — inside another product: your CRM, an intranet, a partner portal or a website. It is read-only, and every view needs a short-lived signed session minted by your server. Calendar embeds are available on the Network plan and on dedicated Enterprise deployments.

  • Who it's for: developers connecting Genkō to a CRM or site where staff or partners need to see the schedule without signing in to Genkō.
  • One embed per boundary, not per view. Each embed defines who may show it (an exact list of site origins) and how much they see (busy only, service, or full patient detail). Up to 10 embeds per organization on Network (50 on a dedicated deployment). To show one provider or one view, narrow at runtime instead of creating more embeds.
  • Clicks reach your page. Clicking an appointment can open a read-only detail panel and sends an appointment:click event with Genkō's id and your external_id, so your CRM can open its own record.
Not the chat widget. The chat widget is a public assistant that helps patients book on your website. A calendar embed is a signed, read-only view of the schedule for staff and partners: it never books, and it has no public mode.

How it works

Your server exchanges an API key for a short-lived token; the browser only ever sees the token.

 Your backend
 1. POST /api/v1/embeds/{id}/sessions ──▶  checks key, scope, IP range and plan
    Authorization: Bearer genko_…     ◀──  201 { token, expires_at }   (15 min by default)

 Your page (browser)
 2. embed.js → getToken() → Your backend → token
 3. <iframe src="…/embed/emb_…#t=token" referrerpolicy="origin">
                                      ──▶  checks the page origin against the allowlist
 4. iframe → GET /api/embed/emb_…/appointments   (Authorization: Bearer token)
                                      ──▶  re-checks embed, key, plan and origin on EVERY request
                                      ◀──  appointments, projected to the detail level
 5. iframe → Your page (browser): ready, resize, appointment:click, token:expired, error  (postMessage)
  • The API key stays on your server. It is never in the page, the script tag, a data attribute or the iframe URL. Use a key holding only the embeds:session scope and pin it to your server's IP range; such a key cannot read anything over the REST API.
  • Tokens are short-lived: 15 minutes by default, 60 at most, and never longer than the embed itself. Each is bound to one embed, one organization, the key that minted it and the embed's allowed origins. Tokens are read-only and are accepted by the embed data endpoint only — never by /api/v1.
  • The origin allowlist is enforced three times: when the iframe loads (the page's origin, sent thanks to referrerpolicy="origin"), in the iframe's messaging (it only talks to that parent), and on every data request (the origin must be in the token and still on the embed).
  • Kill switches work within seconds: revoking the key, disabling, deleting or expiring the embed, removing an origin, or the organization dropping below Network ends live sessions on their next request.

Quickstart in 5 steps

  1. Create an embed in Settings → Calendar embeds. Start from a preset, add the exact origin of every page that will show it (for example https://crm.example.com), choose the detail level and press Save. The live preview shows exactly what partners will see.
  2. Create an API key in Settings → Developers → REST API keys with only the embeds:session scope, and restrict it to your server's IP range. Store it as a server secret (GENKO_API_KEY).
  3. Add one backend endpoint that authenticates your user and calls POST /api/v1/embeds/{id}/sessions — start from one of the examples below.
  4. Paste the snippet into your CRM or page: the embed.js script plus Genko.embed.mount(...), with getToken calling your endpoint. The builder shows it with your embed id filled in.
  5. Verify with the builder's origin test: paste the address of the page where you embedded it and check it says Allowed. Then open the page: the calendar appears, and clicking an appointment fires appointment:click.

Backend token endpoint

Your endpoint is the only place the API key is used. It authenticates your own user first, then mints a token for them and returns it to the browser.

  • POST /api/v1/embeds/{id}/sessions with Authorization: Bearer genko_… and a JSON body: viewer (required, see below), optional providers (up to 50 provider ids), optional view (day, week, month or agenda) and optional ttl_seconds (60–3600, default 900).
  • The viewer is your user, not a Genkō account. The people using your CRM have no Genkō login, so your server says who is looking: id is your own stable user id (for example crm-user-42) and label a readable name or email (for example Dana Smith), both required, 1–128 characters. Genkō does not check them against anything — it trusts them because your key minted the token. They are signed into the token and written to the practice's audit log with the key id: when the token is minted, and on every full-detail data request that returns patients, together with the patient ids. That is how the practice can tell which of your users saw which patients. Keep id the same for a person over time. The viewer grants no access and is not shown in the calendar: what it shows comes from the embed, the key and the providers/view narrowing.
  • Narrowing only shrinks. providers must be within the embed's providers and view one of its views; anything wider is refused with 400 VALIDATION_ERROR instead of being silently ignored.
  • A success is 201 with { token, expires_at, embed_id, viewer }. The call is not idempotent and is never replayed from the idempotency cache: every call returns a fresh token.
  • Handle errors: on 401/403 check the key, its scope, its IP range and the plan; on 404 the embed id; on 409 the embed is disabled or expired; on 429 wait Retry-After. Return a generic error to the browser — the loader raises an error event and tries again at the next refresh.

Node.js

// token-server.mjs — Node 18+, no dependencies.
// Run: GENKO_API_KEY=genko_… node token-server.mjs
import http from 'node:http'

const GENKO = 'https://www.getgenko.com'
const EMBED_ID = 'emb_…'
// A REST key holding ONLY the embeds:session scope. It stays on this server.
const API_KEY = process.env.GENKO_API_KEY

async function mintEmbedToken({ viewer, providers, view }) {
  const res = await fetch(`${GENKO}/api/v1/embeds/${EMBED_ID}/sessions`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ viewer, providers, view, ttl_seconds: 900 }),
  })
  const body = await res.json().catch(() => ({}))
  if (!res.ok) throw new Error(`Genkō ${res.status} ${body.error?.code}: ${body.error?.message}`)
  return body.data // { token, expires_at, embed_id, viewer }
}

http.createServer(async (req, res) => {
  if (req.method !== 'POST' || req.url !== '/genko/embed-token') { res.writeHead(404).end(); return }
  // 1. Authenticate YOUR user first (session cookie, SSO, …) and refuse anonymous callers.
  const user = { id: 'crm-user-42', name: 'Dana Smith' } // ← your signed-in user
  try {
    const session = await mintEmbedToken({
      // viewer = who is looking, in YOUR app (Genkō has no account for them). Both fields are
      // required, 1–128 chars, signed into the token and written to the practice's audit log.
      // They grant no access: what the calendar shows comes from the embed and the narrowing below.
      //   id:    your stable user id — keep it the same for a person, so audits can be traced back
      //   label: a readable name or email, so the practice can read the audit log without asking you
      viewer: { id: user.id, label: user.name },
      // Optional narrowing — it can only shrink what the embed allows:
      // providers: ['<provider uuid>'], view: 'week',
    })
    res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
    res.end(JSON.stringify({ token: session.token, expires_at: session.expires_at }))
  } catch (err) {
    console.error(err.message)
    res.writeHead(502, { 'Content-Type': 'application/json' })
    res.end(JSON.stringify({ error: 'embed_token_unavailable' }))
  }
}).listen(3001, () => console.log('Token endpoint on http://localhost:3001/genko/embed-token'))

Python

# token_server.py — Python 3.9+, standard library only.
# Run: GENKO_API_KEY=genko_… python3 token_server.py
import json
import os
import urllib.error
import urllib.request
from http.server import BaseHTTPRequestHandler, HTTPServer

GENKO = "https://www.getgenko.com"
EMBED_ID = "emb_…"
# A REST key holding ONLY the embeds:session scope. It stays on this server.
API_KEY = os.environ["GENKO_API_KEY"]


def mint_embed_token(viewer, providers=None, view=None, ttl_seconds=900):
    body = {"viewer": viewer, "ttl_seconds": ttl_seconds}
    if providers:
        body["providers"] = providers  # narrowing: can only shrink what the embed allows
    if view:
        body["view"] = view
    req = urllib.request.Request(
        f"{GENKO}/api/v1/embeds/{EMBED_ID}/sessions",
        data=json.dumps(body).encode(),
        method="POST",
        headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
    )
    try:
        with urllib.request.urlopen(req, timeout=10) as res:
            return json.load(res)["data"]  # {token, expires_at, embed_id, viewer}
    except urllib.error.HTTPError as e:
        err = (json.loads(e.read() or b"{}").get("error") or {})
        raise RuntimeError(f"Genkō {e.code} {err.get('code')}: {err.get('message')}") from None


class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        if self.path != "/genko/embed-token":
            self.send_error(404)
            return
        # 1. Authenticate YOUR user first and refuse anonymous callers.
        user = {"id": "crm-user-42", "name": "Dana Smith"}  # ← your signed-in user
        # viewer = who is looking, in YOUR app (Genkō has no account for them). Both fields are
        # required, 1–128 chars, signed into the token and written to the practice's audit log.
        # They grant no access: what the calendar shows comes from the embed and its narrowing.
        #   id:    your stable user id — keep it the same for a person, so audits can be traced back
        #   label: a readable name or email, so the practice can read the audit log without asking you
        viewer = {"id": user["id"], "label": user["name"]}
        try:
            session = mint_embed_token(viewer)
            # e.g. mint_embed_token(viewer, providers=["<provider uuid>"], view="week")
            status, payload = 200, {"token": session["token"], "expires_at": session["expires_at"]}
        except Exception as exc:
            self.log_error("%s", exc)
            status, payload = 502, {"error": "embed_token_unavailable"}
        data = json.dumps(payload).encode()
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Cache-Control", "no-store")
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)


HTTPServer(("127.0.0.1", 3001), Handler).serve_forever()

PHP

<?php
// genko-embed-token.php — PHP 7.4+ with the curl extension. Route POST /genko/embed-token here.
// Set GENKO_API_KEY=genko_… in the environment: a REST key holding ONLY the embeds:session scope.
const GENKO = 'https://www.getgenko.com';
const EMBED_ID = 'emb_…';

function mint_embed_token(array $viewer, ?array $providers = null, ?string $view = null): array {
    $body = ['viewer' => $viewer, 'ttl_seconds' => 900];
    if ($providers) $body['providers'] = $providers; // narrowing: can only shrink what the embed allows
    if ($view) $body['view'] = $view;

    $ch = curl_init(GENKO . '/api/v1/embeds/' . EMBED_ID . '/sessions');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 10,
        CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('GENKO_API_KEY'), 'Content-Type: application/json'],
        CURLOPT_POSTFIELDS => json_encode($body),
    ]);
    $raw = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $error = curl_error($ch);
    curl_close($ch);

    if ($raw === false) throw new RuntimeException("Genkō unreachable: $error");
    $json = json_decode($raw, true);
    if ($status !== 201) {
        throw new RuntimeException("Genkō $status " . ($json['error']['code'] ?? '') . ': ' . ($json['error']['message'] ?? ''));
    }
    return $json['data']; // token, expires_at, embed_id, viewer
}

// 1. Authenticate YOUR user first and refuse anonymous callers.
$user = ['id' => 'crm-user-42', 'name' => 'Dana Smith']; // ← your signed-in user

header('Content-Type: application/json');
header('Cache-Control: no-store');
// viewer = who is looking, in YOUR app (Genkō has no account for them). Both fields are
// required, 1–128 chars, signed into the token and written to the practice's audit log.
// They grant no access: what the calendar shows comes from the embed and its narrowing.
//   id:    your stable user id — keep it the same for a person, so audits can be traced back
//   label: a readable name or email, so the practice can read the audit log without asking you
$viewer = ['id' => $user['id'], 'label' => $user['name']];
try {
    $session = mint_embed_token($viewer);
    // e.g. mint_embed_token($viewer, ['<provider uuid>'], 'week');
    echo json_encode(['token' => $session['token'], 'expires_at' => $session['expires_at']]);
} catch (Throwable $e) {
    error_log($e->getMessage());
    http_response_code(502);
    echo json_encode(['error' => 'embed_token_unavailable']);
}

curl

# The key stays on your server: a REST key holding ONLY the embeds:session scope.
export GENKO_API_KEY=genko_…

# Mint a token for one viewer (15 minutes by default, at most 60).
# viewer = who is looking, in YOUR app: id = your stable user id, label = a readable name or
# email (both required, 1–128 chars). Signed into the token and written to the practice's
# audit log; they grant no access.
curl -sS -X POST "https://www.getgenko.com/api/v1/embeds/emb_…/sessions" \
  -H "Authorization: Bearer $GENKO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"viewer":{"id":"crm-user-42","label":"Dana Smith"}}'

# Narrowed: only one provider, locked to the week view, 30 minutes:
curl -sS -X POST "https://www.getgenko.com/api/v1/embeds/emb_…/sessions" \
  -H "Authorization: Bearer $GENKO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"viewer":{"id":"crm-user-42","label":"Dana Smith"},"providers":["<provider uuid>"],"view":"week","ttl_seconds":1800}'

# → 201 {"data":{"token":"eyJ…","expires_at":"…","embed_id":"emb_…","viewer":{…}},"error":null,"meta":{…}}
Never call this endpoint from a browser, and never ship the key to a client (mobile apps included). If a key leaks, revoke it: every session it minted stops within seconds.

Frontend integration

The loader (recommended)

Load embed.js from Genkō and mount the calendar into an element. getToken is your function: it asks your backend for a token and returns a Promise of the token string. The loader calls it on load and again about a minute before each token expires, so the calendar never goes stale.

<div id="genko-calendar"></div>
<script src="https://www.getgenko.com/embed.js"></script>
<script>
  var calendar = Genko.embed.mount(document.getElementById('genko-calendar'), {
    embed: 'emb_…',
    // Called on load and again shortly before each token expires.
    // It must ask YOUR server for a token — never put an API key in the page.
    getToken: function () {
      return fetch('/genko/embed-token', { method: 'POST', credentials: 'same-origin' })
        .then(function (res) {
          if (!res.ok) throw new Error('Token request failed: ' + res.status)
          return res.json()
        })
        .then(function (data) { return data.token })
    },
    // Optional: view: 'week', providers: ['<provider uuid>']
    // Optional: theme: 'dark' — overrides the theme configured on the embed; omit it to use that one
  })

  calendar.on('appointment:click', function (appointment) {
    // appointment.id is Genkō's id; appointment.external_id is the id you set via the API (or null)
    console.log('Clicked', appointment.id, appointment.external_id)
  })
  calendar.on('error', function (err) { console.warn('Calendar embed error:', err.code) })
</script>
  • The loader creates the iframe with referrerpolicy="origin" and passes the token in the URL fragment (#t=…), which browsers never send to a server. The iframe removes it from its address immediately.
  • The iframe resizes itself to its content (unless you pass height or the embed uses a fixed height).
  • Include the script once per page; mount as many calendars as you like.

Plain <iframe> (for CRMs that strip scripts)

If your CRM accepts only an iframe, render this on your server with a freshly minted token.

<!-- For CRMs that strip <script>. Render this on YOUR server with a fresh token.
     It cannot refresh itself: after the token expires (15 min by default) the viewer must reload. -->
<iframe
  src="https://www.getgenko.com/embed/emb_…#t=YOUR_TOKEN"
  referrerpolicy="origin"
  title="Calendar"
  style="width:100%;height:720px;border:0">
</iframe>
  • No auto-refresh: when the token expires (15 minutes by default, up to 60 with ttl_seconds) the calendar shows Session expired until the page is reloaded. Mint the token when the page is rendered, never in advance.
  • No events reach your page (there is no loader to relay them), and the height is fixed.
  • Keep referrerpolicy="origin": without it some browsers send no origin, and the embed refuses to load.

A standalone test page

To check an embed without touching your CRM, save this page as test.html, serve it from one of the embed's allowed origins and paste a token from the curl example. In production, origins must be https: use a host you control or an https tunnel.

<!doctype html>
<!-- Save as test.html and serve it from one of the embed's allowed origins (the page's origin
     must match exactly). Paste a token from the curl example; it is valid for 15 minutes. -->
<html>
<head><meta charset="utf-8"><title>Genkō calendar embed test</title></head>
<body style="font-family:system-ui;margin:24px">
  <h1>Calendar embed test</h1>
  <div id="genko-calendar"></div>
  <pre id="log"></pre>
  <script src="https://www.getgenko.com/embed.js"></script>
  <script>
    var TOKEN = 'YOUR_TOKEN' // from the curl example
    var log = function (m) { document.getElementById('log').textContent += m + '\n' }
    Genko.embed.mount(document.getElementById('genko-calendar'), {
      embed: 'emb_…',
      getToken: function () { return Promise.resolve(TOKEN) }
    })
      .on('ready', function () { log('ready') })
      .on('appointment:click', function (a) { log('click ' + a.id + ' ' + a.external_id) })
      .on('token:expired', function () { log('token expired — paste a new one and reload') })
      .on('error', function (e) { log('error ' + e.code) })
  </script>
</body>
</html>

CRM and site notes

  • CRMs that allow custom HTML or scripts: use the loader snippet. Put the token endpoint behind your CRM's own login (its backend, a serverless function or your integration server).
  • CRMs that allow only an iframe or a URL widget: use the plain iframe rendered server-side with a fresh token, or point the widget at a small page on your own domain that uses the loader — that page's origin is the one you add to the embed.
  • Editors that strip scripts (rich-text fields, some CMS blocks): same as iframe-only. If even iframes are stripped, link to a page on your domain instead.
  • The origin is the page's, not the vendor's. If the calendar appears on https://acme.examplecrm.com/…, add https://acme.examplecrm.com. Add every host it may appear on (for example both https://app.example.com and https://www.example.com).
  • We don't document vendor-specific click paths — they change often. Look for your CRM's custom HTML, iframe widget or web tab feature.

If the page has its own Content Security Policy, allow Genkō as a frame source (and as a script source for the loader). The iframe makes its own requests to Genkō, so connect-src needs nothing.

Content-Security-Policy: frame-src https://www.getgenko.com; script-src 'self' https://www.getgenko.com

Genkō's side is already configured: /embed/* may be framed by any https page, and the embed itself decides whether to render for your origin.

Configuration reference

Security-relevant settings are stored as their own fields and checked by the database; presentation settings live in a versioned config object. Both are edited in the builder.

Access and data

SettingTypeDefaultLimitsDescription
namestring—1–80Internal label (shown in Settings, never to partners).
activebooleantrueSwitch the embed off without deleting it; live sessions stop within seconds.
expires_attimestamp | nullnull> nowOptional end date. Sessions never outlive it.
allowed_originsstring[]—1–10Exact origins allowed to show the embed: https://host[:port], lowercase, no path, no trailing slash, no wildcards.
provider_idsuuid[][] = all≤ 100Only these providers' appointments. Tokens and the loader's providers can narrow further.
service_idsuuid[][] = all≤ 100Only these services' appointments.
statusesstring[]scheduled, confirmed, completed, pending_payment1–6Which appointment statuses appear (cancelled and no_show are hidden by default).
window_days_backinteger70–90How far into the past the embed can ever show, whatever range is requested.
window_days_aheadinteger600–366How far into the future it can show.
detail_levelbusy | service | fullbusyThe most any session may see — see the table below.

Presentation (config)

SettingTypeDefaultLimitsDescription
viewsday week month agendaall four1–4Views the viewer can switch between.
default_viewviewweek∈ viewsThe view it opens in (the loader's view or a token's view can pick another enabled view).
week_startmonday | sundaymondayFirst day of the week in the week and month views.
hours{ start, end }{ 7, 20 }0 ≤ start < end ≤ 24Visible hours of the day and week grids.
weekendsbooleantrueShow Saturday and Sunday in the week view.
timezoneorg | provider | viewerorgorg: the practice's time zone; provider: the provider's, when narrowed to one; viewer: the device's.
localeen | esenInterface language.
themelight | dark | autolightauto follows the viewer's device. The loader's theme overrides it.
primary_colorhex#4f46e5#RRGGBBAccent colour. Hex only — there is no custom CSS.
color_byprovider | service | statusproviderHow appointments are coloured. Busy embeds always colour by provider.
toolbar{ navigation, today, view_switcher, title }all trueShow or hide previous/next, Today, the view switcher and the title.
height{ mode: auto | fixed, px }auto, 720px 320–2000auto fits the content (the loader resizes the iframe); fixed uses px and scrolls inside.
clickpanel | event | nonepanelpanel: open a read-only detail panel and send appointment:click; event: only send the event; none: nothing.
actions{ confirm, reschedule, cancel }all falsefalseReserved for confirm, reschedule and cancel. Must be false; not available yet.

What each detail level exposes

LevelFieldsNotes
busyid, external_id, starts_at, ends_at, provider { id, full_name }Only that a time is taken, and by which provider. No service, status or patient.
service+ service { id, name, color }, statusAdds the service name and colour and the appointment status. Still no patient.
full+ patient { id, external_id, full_name, email, phone }Adds the patient's name, email and phone. Every data request at this level writes a detailed audit entry naming the viewer. Use it only where the viewers may see patient identities.

JavaScript API and events

embed.js defines one global, Genko.embed. It is plain ES5, so it also runs in older CRM browsers.

var calendar = Genko.embed.mount(element, {
  embed: 'emb_…',           // required
  getToken: function (ctx) {          // required: returns a Promise of a token string
    // ctx = { embed, view, providers } — pass them to your backend to narrow the token
    return fetch('/genko/embed-token', { method: 'POST' }).then(function (r) { return r.json() })
      .then(function (d) { return d.token })
  },
  view: 'week',                       // optional: start in this view (one of the embed's views)
  providers: ['<provider uuid>'],     // optional: show fewer providers (never more than the token allows)
  theme: 'dark',                      // optional: light | dark | auto — OVERRIDES the embed's configured theme; omit to use it
  height: 640,                        // optional: fixed pixel height (omit to auto-resize)
  title: 'Team calendar'              // optional: the iframe's accessible title
})

calendar.on('ready', function (e) { /* { embed, view } */ })
calendar.on('appointment:click', function (a) { /* { id, external_id, starts_at, ends_at } */ })
calendar.on('token:expired', function (e) { /* { reason } — the loader is already fetching a new token */ })
calendar.on('resize', function (e) { /* { height } */ })
calendar.on('error', function (e) { /* { code, message? } */ })

calendar.update({ view: 'day', providers: ['<provider uuid>'], theme: 'dark' })
calendar.off('appointment:click')     // remove all handlers for an event (or pass the handler)
calendar.destroy()                    // remove the iframe and every listener
  • mount(element, options) returns an instance. It throws at once on a missing element, an invalid embed id or a missing getToken.
  • on(event, handler) / off(event, handler?) subscribe and unsubscribe; unknown event names throw. An exception in one handler never stops the others.
  • update({ view, providers, theme }) changes the calendar at runtime. Changing view or providers also calls getToken again with the new values, so your backend can mint a narrower token.
  • destroy() removes the iframe and every listener.
  • Events: ready (first data shown), appointment:click (with id and external_id — the id you set on the appointment through the API, or null), token:expired (the loader is already fetching a new token), resize and error (codes below).

The genko:v1 message protocol

The loader and the iframe talk over postMessage; every message carries ns: "genko:v1". The loader accepts messages only from its own iframe at Genkō's origin; the iframe accepts them only from the parent origin the server verified, and refuses to render if a genko:v1 message arrives from anywhere else. You only need this to build your own loader.

// iframe → your page (checked: event.origin === embed origin && event.source === the iframe)
{ ns: 'genko:v1', type: 'init' }                      // asks the loader for its options
{ ns: 'genko:v1', type: 'ready', view: 'week' }
{ ns: 'genko:v1', type: 'resize', height: 812 }
{ ns: 'genko:v1', type: 'appointment:click', appointment: { id, external_id, starts_at, ends_at } }
{ ns: 'genko:v1', type: 'token:expired', reason: 'token_expired' }
{ ns: 'genko:v1', type: 'error', code: 'origin_not_allowed' }

// your page → iframe (accepted only from the parent origin the server verified)
{ ns: 'genko:v1', type: 'options', view, providers, theme }
{ ns: 'genko:v1', type: 'token', token: 'eyJ…' }

Errors and troubleshooting

Every response has the shape { data, error: { code, message, details? }, meta: { request_id } }. Branch on error.code (stable) and, for the data endpoint, error.details.reason — never on the message. Quote meta.request_id when you contact support.

POST /api/v1/embeds/{id}/sessions

CodeHTTPWhen
UNAUTHORIZED401Missing, malformed, unknown, revoked or expired key; an MCP key (genko_mcp_…); or an embed token sent as the key.
FORBIDDEN403The key lacks embeds:session (details.required_scope), or the request came from an IP outside the key's allowlist.
PLAN_LIMIT403The organization is not on Network (details.required_plan: "enterprise", the stable identifier of the Network plan).
NOT_FOUND404No such embed in the key's organization.
CONFLICT409The embed is disabled (details.reason: "embed_inactive") or expired ("embed_expired").
VALIDATION_ERROR400Invalid body (missing viewer, unknown field, ttl_seconds out of range) or narrowing wider than the embed (details.not_in_embed, details.enabled_views).
INVALID_JSON400The body is not valid JSON.
UNSUPPORTED_MEDIA_TYPE415Missing Content-Type: application/json.
PAYLOAD_TOO_LARGE413Body over 1 MB.
RATE_LIMITED429Over the key's write rate limit; wait Retry-After seconds.
INTERNAL500Our fault; retry with backoff.

GET /api/embed/{id}/appointments (called by the iframe)

CodeHTTPWhen
UNAUTHORIZED401details.reason: missing_token, invalid_token, token_expired (the loader fetches a new one) or session_revoked (the key was revoked or lost embeds:session).
FORBIDDEN403origin_not_allowed (the origin is not in the token or no longer on the embed) or embed_inactive (disabled or expired).
PLAN_LIMIT403The organization dropped below Network.
VALIDATION_ERROR400Range over 31 days, from not before to, an unknown query parameter, or a credential in the URL.
RATE_LIMITED429More than 120 requests a minute for this embed from one IP.
INTERNAL500Our fault; the calendar offers Try again.

Loader error event codes

codeMeaning
origin_not_allowedThe page's origin is not on the embed's list (or the browser sent no origin).
embed_inactiveThe embed is disabled or expired.
plan_requiredThe organization is not on Network.
not_foundThe embed id does not exist.
token_fetch_failedOn first load, your getToken rejected or returned something that is not a token.
token_refresh_failedThe same while refreshing; the calendar keeps working until the old token expires.
network_errorThe iframe could not reach Genkō.

Common problems

The frame says This calendar can't be shown here
The page's origin is not on the embed's list. Compare exactly — scheme, host and port: https://www.example.com and https://example.com are different origins, and so are http and https. Use the builder's origin test with the page's full address.
… refused to connect (the browser's own error inside the frame)
The iframe points at a page that cannot be framed — usually a dashboard URL instead of /embed/emb_… — or your page's Content Security Policy blocks Genkō in frame-src (the browser console says which).
Session expired
The token ran out. With the loader this lasts only until getToken returns a new one; if it stays, your token endpoint is failing (look for token_refresh_failed). With a plain iframe, reload the page.
The session exchange returns 403 FORBIDDEN with required_scope
The key does not hold embeds:session. Create a key with that scope (keys cannot be edited).
403 PLAN_LIMIT
Calendar embeds need the Network plan (or a dedicated Enterprise deployment). The plan is checked on every request, so a downgrade stops embeds at once.
Saving an embed fails with Each origin must be exact
Remove paths, trailing slashes and wildcards: https://crm.example.com, not https://crm.example.com/ or https://*.example.com.
Nothing appears and there is no error
Check the embed's filters (providers, services, statuses) and its window (days back and ahead): a range outside the window returns no appointments rather than an error.

Limits and security

  • Tokens: 15 minutes by default, 60 seconds to 60 minutes with ttl_seconds, never past the embed's expiry.
  • Data: at most 31 days per request (the iframe splits a month grid into two requests) and 999 appointments per request (beyond that, the calendar says it shows the first ones). The embed's window — up to 90 days back and 366 ahead — caps everything.
  • Rate limits: the data endpoint allows 120 requests a minute per embed and IP; the session exchange counts against the key's REST write limit (Network: 180 a minute per key).
  • Embeds: 10 per organization on Network (50 on a dedicated deployment), each with up to 10 origins.
  • Patient data by level: busy none; service none; full name, email and phone — audited on every request with the viewer's id.
Clinical data is never included, at any level: no clinical or appointment notes, intake answers, allergies, medications, conditions, insurance, date of birth or address. The data endpoint never even selects those columns.

Roadmap

  • Actions — confirm, reschedule and cancel from inside the embed — are planned and not available yet. The configuration already reserves them (config.actions), so turning them on later will not need a new embed.
  • Also planned: binding an embed to specific API keys, and creating a dedicated key from the builder.
  • Not planned for now: a public (anonymous) mode and custom CSS.

API & AI (MCP)

What is the MCP API?

Genkō exposes a Model Context Protocol (MCP) server that lets external AI agents and tools read and write your practice data in a structured, tool-call format. This is the same underlying registry used by the dashboard AI assistant — so any agent you connect gets access to the same capabilities.

The MCP endpoint is a stateless HTTP server compatible with any client that speaks the MCP Streamable HTTP transport (e.g. Claude Desktop, OpenAI Agents SDK, or any custom agent built with @modelcontextprotocol/sdk).

Plan requirements: MCP access is available on the Group plan and above. Group gets read-only access (1 key). Practice+ gets read + write (3 keys). Network gets all scopes and unlimited keys. MCP keys and REST API keys are limited separately.
Separate from the REST API: MCP keys work only on /api/mcp, and REST API keys only on /api/v1. Each kind has its own panel in Settings → Developers and its own key limit, but both follow the same scopes and plan rules, so a tool and the matching endpoint always behave alike.

Quick start

1. Go to Settings → Developers and scroll to the MCP keys panel.
2. Click Add MCP key, give the key a name (e.g. "Claude Desktop"), and choose a scope.
3. Copy the raw key shown immediately — it is only displayed once.
4. In your MCP client, set the server URL and the Authorization header:

# Claude Desktop — claude_desktop_config.json

{
  "mcpServers": {
    "genko": {
      "url": "https://www.getgenko.com/api/mcp",
      "headers": {
        "Authorization": "Bearer genko_mcp_YOUR_KEY_HERE"
      }
    }
  }
}
Org selection: Genkō automatically resolves the organization from your API key — no org slug in the URL needed.

Connect Claude or ChatGPT by signing in (no key to paste)

For AI apps with custom connectors, add https://www.getgenko.com/api/mcp as the connector URL and choose Sign in. Genkō shows a consent screen to the signed-in owner or admin: pick the organization, pick an access level (Read, Read + Write or Admin, within your plan) and accept the terms and the patient-data acknowledgement. The app never sees a key; it gets a short-lived token bound to that one organization and access level.

  • Off until the owner turns it on: Settings → Developers → AI connectors (owner only). Turning it off disconnects every app at once.
  • Same rules as keys: only owners and admins who may manage API keys can approve a connection, the plan gates apply on every request, and Admin access (clinical records) needs the Network plan.
  • Managed with your keys: each connection is listed in the MCP panel and can be revoked there. It also stops working if the person who approved it is removed or loses API access, or the plan no longer includes the access level.
  • Keys still work for Claude Desktop config files, scripts and any client without OAuth support.

Authentication

All requests to /api/mcp must include a valid API key as a Bearer token:

Authorization: Bearer genko_mcp_...

MCP keys are created in Settings → Developers → MCP keys. Each key has a scope:

  • Read — can call list/get tools only (Group plan)
  • Read + Write — can also create and update records (Practice+ plan)
  • Admin — everything above plus clinical records and patient deletion (Network)

Keys can be revoked at any time. Revocation is immediate — in-flight requests are not interrupted, but subsequent requests with the revoked key return 401 Unauthorized.

Available tools

The tools exposed depend on the scope of your API key. All tools operate on data scoped to your organization only.

Read tools (available on all scopes):

  • list_appointments — list appointments, filtered by patient, provider, service, status, external ID, one date, a relative range (today, this_week, next_7_days, next_30_days) or a from/to window
  • get_appointment — fetch a single appointment by ID with full details
  • list_patients — search patients by name, email or phone (3+ characters) or look one up by mrn or external_id; paged with meta.next_cursor
  • get_patient — fetch one patient by ID (clinical fields such as notes and allergies only with clinical access)
  • list_providers — list all providers with their specialties
  • get_provider — fetch a provider's profile and weekly availability schedule
  • list_services — list all appointment types (services) with durations and prices
  • check_availability — return open time slots for a provider on a given date
  • search_icd10cm_codes — search diagnosis codes to code a condition: the practice's own system (ICD-10-CM, or CIE-10 once the Spanish catalog is available), or force one with system (searched inside Genkō; nothing is sent to a third party)
  • get_org_info — the organization's id, name, slug and time zone (offered to every key)
  • hubspot_get_status — whether HubSpot is connected to the organization and, if so, its portal ID and connection date (offered to every key; { connected: false } when it is not)
List tools return { data, meta: { next_cursor, has_more, limit } } and accept cursor; pass meta.next_cursor to get the next page. The defaults, response shapes, time-zone rules and the service / appointment type naming are in the tool reference below.

Write tools (read + write or admin scope, Practice+ required):

  • book_appointment — book a new appointment (conflict-checked)
  • update_appointment — change the time, status, or notes of an existing appointment (conflict-checked)
  • cancel_appointment — set an appointment to cancelled status
  • update_patient — update a patient's name, email, phone, date of birth, or notes (pass null to clear a field); with clinical write access also the coded allergy, condition and medication lists
  • find_or_create_patient — look up by phone, email or name, optionally auto-create if not found
  • hubspot_sync_patient — upsert a patient as a HubSpot contact (matched by email) and return the contact ID; needs HubSpot connected and a patient email
  • hubspot_log_appointment_note — log an appointment event as a note on the patient's HubSpot contact, syncing the contact first if needed; needs HubSpot connected and a patient email
HubSpot tools: HubSpot sync is a Starter+ integration, connected in Settings → Integrations. The two write tools need a read + write key (Practice+) and fail with an error when HubSpot is not connected; hubspot_get_status never fails for that reason.
Conflict detection: book_appointment and update_appointment run server-side conflict checks. If the requested slot is already taken, the tool returns a CONFLICT error instead of double-booking.
Status rules: completed, cancelled and no_show appointments are final — update_appointment and cancel_appointment return an INVALID_TRANSITION error instead of reopening them. Cancelling an already-cancelled appointment succeeds without notifying the patient again. Tool errors include a stable code (for example NOT_FOUND, CONFLICT, INVALID_TRANSITION, VALIDATION_ERROR) you can branch on.

Clinical Records tools (admin key, Network required — the same rule as the REST clinical endpoints):

  • get_patient_record — fetch the full longitudinal record for a patient: every visit with its clinical notes and intake form in chronological order
  • list_appointment_notes — list all clinical notes for an appointment (SOAP, progress, intake review, discharge)
  • get_clinical_note — fetch a single clinical note by ID with full SOAP fields or free-form content
  • get_appointment_intake — fetch the pre-visit intake form submitted by a patient for an appointment
  • create_clinical_note — create a draft SOAP or free-form clinical note for an appointment; a clinician signs it in the dashboard (is_draft=false is refused with NOTE_SIGN_REQUIRES_CLINICIAN); it can carry a specialty template (template_key + template_data)
  • update_clinical_note — edit a draft note (pass its appointment_id); a signed note returns NOTE_LOCKED and is never changed
  • add_clinical_addendum — add a draft addendum to a signed note (a correction or late entry); the original note never changes

Admin tools (admin scope, Network+ required):

  • review_intake — approve or reject a patient intake submission; rejected submissions prompt the patient to revise before the visit

Tool reference

Every tool with its parameters, defaults and result. This table is checked against the server on every build, so it cannot drift from what tools/list returns. A key sees only the tools its scope and your plan allow.

ToolNeedsParametersReturns
get_org_infoany keynone{ id, name, slug, timezone }
list_servicesservices:read
Group+
  • cursor string · optional
  • limit integer · 1–100 · default 100 · optional
  • external_id string · optional
{ data, meta }
list_providersproviders:read
Group+
  • cursor string · optional
  • limit integer · 1–100 · default 100 · optional
  • external_id string · optional
{ data, meta }
check_availabilityscheduling:read
Group+
  • provider_id uuid · required
  • date date · YYYY-MM-DD · required
  • duration_minutes integer · 5–480 · default 30 · optional
  • appointment_type_id uuid · optional
[{ start, end, starts_at, ends_at, timezone }]
list_appointmentsscheduling:read
Group+
  • patient_id uuid · optional
  • provider_id uuid · optional
  • service_id uuid · optional
  • external_id string · optional
  • range enum · today | this_week | next_7_days | next_30_days · optional
  • date date · YYYY-MM-DD · optional
  • from datetime · optional
  • to datetime · optional
  • status enum · scheduled | confirmed | cancelled | completed | no_show · optional
  • limit integer · 1–100 · default 50 · optional
  • cursor string · optional
{ data, meta }
get_appointmentscheduling:read
Group+
  • appointment_id uuid · required
appointment
list_patientspatients:read
Group+
  • query string · optional
  • email string · optional
  • phone string · optional
  • mrn string · optional
  • external_id string · optional
  • include_total boolean · optional
  • limit integer · 1–50 · default 20 · optional
  • cursor string · optional
{ data, meta }
get_patientpatients:read
Group+
  • patient_id uuid · required
patient
get_providerproviders:read
Group+
  • provider_id uuid · required
provider
search_icd10cm_codespatients:read
Group+
  • query string · required
  • system enum · diagnosis | icd10cm | cie10 · default diagnosis · optional
  • limit integer · 1–50 · default 10 · optional
{ release, system, results }
hubspot_get_statusany keynone{ connected, portal_id?, connected_at? }
find_or_create_patientpatients:write
Practice+
  • phone string · E.164 · optional
  • email string · optional
  • full_name string · optional
  • auto_create boolean · default false · optional
{ found, patient | patients, created? }
book_appointmentscheduling:write
Practice+
  • patient_id uuid · required
  • provider_id uuid · required
  • starts_at datetime · UTC, Z · required
  • ends_at datetime · UTC, Z · required
  • appointment_type_id uuid · optional
  • notes string · optional
{ booked: true, appointment }
cancel_appointmentscheduling:write
Practice+
  • appointment_id uuid · required
{ cancelled: true, already_cancelled?, appointment }
update_appointmentscheduling:write
Practice+
  • appointment_id uuid · required
  • starts_at datetime · UTC, Z · optional
  • ends_at datetime · UTC, Z · optional
  • status enum · scheduled | confirmed | completed | no_show · optional
  • notes string · optional
{ updated: true, appointment }
update_patientpatients:write
Practice+
  • patient_id uuid · required
  • full_name string · optional
  • email string · null clears · optional
  • phone string · null clears · optional
  • date_of_birth date · YYYY-MM-DD · null clears · optional
  • notes string · null clears · optional
  • allergies array · optional
  • conditions array · optional
  • medications array · optional
{ updated: true, patient, updated_fields }
hubspot_sync_patientpatients:write
Practice+
  • patient_id uuid · required
{ synced: true, contact_id }
hubspot_log_appointment_notescheduling:write
Practice+
  • appointment_id uuid · required
{ logged: true, contact_id }
get_patient_recordclinical:read
Network
  • patient_id uuid · required
  • limit integer · 1–200 · default 100 · optional
[visit]
list_appointment_notesclinical:read
Network
  • appointment_id uuid · required
[note]
get_clinical_noteclinical:read
Network
  • note_id uuid · required
note
get_appointment_intakeclinical:read
Network
  • appointment_id uuid · required
intake | { status: "not_submitted" }
create_clinical_noteclinical:write
Network
  • appointment_id uuid · required
  • patient_id uuid · required
  • note_type enum · soap | progress | intake_review | discharge · default progress · optional
  • is_draft boolean · default true · optional
  • subjective string · optional
  • objective string · optional
  • assessment string · optional
  • plan_text string · optional
  • content string · optional
  • vitals object · optional
  • template_key string · null clears · optional
  • template_data object · optional
{ created: true, note }
update_clinical_noteclinical:write
Network
  • appointment_id uuid · required
  • note_id uuid · required
  • note_type enum · soap | progress | intake_review | discharge · optional
  • is_draft boolean · optional
  • subjective string · null clears · optional
  • objective string · null clears · optional
  • assessment string · null clears · optional
  • plan_text string · null clears · optional
  • content string · null clears · optional
  • vitals object · optional
  • template_key string · null clears · optional
  • template_data object · optional
{ updated: true, note }
add_clinical_addendumclinical:write
Network
  • appointment_id uuid · required
  • note_id uuid · required
  • content string · required
{ created: true, addendum }
review_intakeclinical:write
Network
  • appointment_id uuid · required
  • status enum · approved | rejected · required
  • review_notes string · optional
{ reviewed: true, intake }

Conventions that apply to every tool

  • Response shapes. List tools return { data, meta: { next_cursor, has_more, limit } }. check_availability returns a bare JSON array of slots, with no wrapper. Write tools return an object that names what happened: book_appointment returns { booked: true, appointment }, update_appointment { updated: true, appointment }, cancel_appointment { cancelled: true, appointment }. Single-record reads (get_appointment, get_patient, get_provider) return the record itself. A failed call returns isError: true with { error, code, details? }; branch on code.
  • Page size. Every list tool accepts limit and cursor. Defaults and maximums: list_patients 20 (max 50), list_appointments 50 (max 100), list_services and list_providers 100 (max 100). Pass meta.next_cursor as cursor to continue.
  • Services are appointment types. A service is stored as an appointment type, and the tools use both names for the same UUID: book_appointment and check_availability take appointment_type_id, list_appointments filters with service_id, and every appointment returned carries service_id. Use the id from list_services for all three.
  • Time zones: check, then book. check_availability reads date (YYYY-MM-DD) as a calendar day in the provider’s time zone, or in the organization’s time zone when the provider has none set; the response names the zone used in timezone. Each slot has local start/end and UTC starts_at/ends_at. book_appointment and update_appointment need UTC ISO-8601 timestamps ending in Z: copy starts_at and ends_at from the slot unchanged. A timestamp without an offset is read in the provider’s zone (organization’s as the fallback), but do not rely on that. list_appointments reads date, from and to in the organization’s time zone.
  • Unknown arguments are rejected. Like the REST API, the MCP server refuses a call that carries an argument the tool does not define. The call fails with a JSON-RPC <code>InvalidParams</code> error (-32602) that names the argument, instead of running without it, so a misspelled filter can never silently widen a result. (The dashboard assistant is more forgiving: it drops unknown arguments and treats empty or null values as not provided.)
Authorship on API-created notes: clinical notes created through the API are attributed to the user who created the API key (or to the organization's owner if that user is gone). Every clinical read and write is recorded in the audit log.

Rate limits

MCP API requests are rate-limited per key using a fixed 60-second window. The default limit is 100 requests per minute. If you exceed the limit, the server responds with:

HTTP 429 Too Many Requests
Retry-After: 60

Usage for each key (calls in the last 7 days) is visible in the Settings → Developers → MCP keys panel next to each key name.

Network plans can request a higher rate limit. Contact us at hello@getgenko.com.

Dashboard AI assistant

Group+ organizations can enable a built-in AI assistant directly in the dashboard. When enabled, a floating chat widget appears in the bottom-right corner of every dashboard page for every team member; what it can do depends on their role.

For owners and admins the assistant has full admin-scope tool access, including booking, updating and cancelling appointments, and can answer questions like:

  • “Who is my next patient today?”
  • “Find all appointments for Maria Santos this month”
  • “What slots are available for Dr. Reyes on Friday?”
  • “Update appointment #abc123 to 3pm”

To enable: Go to Settings → Integrations and toggle on Dashboard AI Assistant. The toggle is visible only on the Group plan and above. No API key is required — the assistant uses your session automatically.

Roles and limits: providers and staff get a read-only assistant (providers see their own schedule by default), so it can look things up but never changes a record. Messages are metered per organization per calendar month: Group 500, Practice 2,000, Network unlimited. Current usage is shown in Settings → Billing.

The dashboard assistant talks to your team, never to patients. Patients have their own assistant in the patient portal (Practice+); WhatsApp today handles reminders and simple replies — confirm, reschedule, cancel (see Patient self-booking), and a patient-facing AI assistant over WhatsApp is on the roadmap.

Embeddable chat widget

Practice+ organizations can also embed a public AI chat widget on their own website. Visitors can ask about services, providers, and availability without leaving your site. The widget cannot see or write patient data — it only has access to a public-safe tool scope (e.g. listing services and open time slots).

To enable:

  1. Go to Settings → Integrations and find the Website Chat Widget panel.
  2. Toggle it on.
  3. Add the domains where you'll embed the widget under Allowed origins (e.g. https://yourpractice.com), one per line. Use * to allow any origin.
  4. Copy the embed snippet and paste it before the closing </body> tag of your website:
<script src="https://www.getgenko.com/widget.js" data-org="your-slug" defer></script>
Scope & rate limits: The widget is rate-limited per visitor session and per organization to prevent abuse, and never has write access. It is a separate surface from the dashboard AI assistant above, which is staff-only and uses full admin-scope access.

Account Security

Genkō supports two-factor authentication (2FA) to add a second layer of protection beyond your password or OAuth sign-in. You can enroll, manage, and remove authentication methods at any time from Settings → Security.

Authenticator App (TOTP)

Time-based one-time passwords (TOTP) work with any standards-compliant authenticator app — Google Authenticator, Microsoft Authenticator, 1Password, Authy, and others.

To set up:

  1. Go to Settings → Security and click + Add authenticator app.
  2. Scan the QR code with your app. If you can't scan, copy the manual setup key shown below the code.
  3. Enter the 6-digit code shown in your app and click Confirm setup.

To remove TOTP, click Remove next to the active authenticator. You can re-enroll at any time.

Tip: Back up your TOTP setup key in a secure place (e.g. a password manager). If you lose access to your app and haven't set up a passkey, contact support to regain access.

Passkeys (WebAuthn / FIDO2)

Passkeys let you sign in using your device's built-in authenticator — Face ID, Touch ID, Windows Hello, or a FIDO2 hardware key like a YubiKey. They are phishing-resistant: no shared secret leaves your device.

Passwordless sign-in: Once you have registered a passkey, you can sign in to Genkō without entering a password. On the login page, click Passkey and follow your device's prompts (Face ID, Touch ID, etc.). You don't even need to type your email if you've signed in on that device before.

Two-factor authentication (2FA): Passkeys also act as a powerful second factor. If you sign in with your email and password, and have a passkey registered, Genkō will prompt you for a passkey challenge to complete the sign-in. This ensures that even if your password is compromised, your account remains secure.

To register a passkey:

  1. Go to Settings → Security and optionally enter a nickname for the passkey (e.g. "MacBook Pro", "YubiKey").
  2. Click + Add passkey. Your browser will prompt for biometric confirmation or hardware key touch.
  3. Confirm — the passkey is registered immediately.

You can register multiple passkeys (one per device) and remove any of them individually. Passkeys are stored securely in your account and never transmitted.

Passkey management in security settings
Passkey support requires a modern browser (Chrome 108+, Safari 16+, Firefox 122+, Edge 108+) and a compatible platform authenticator or hardware key.

Setup prompt after sign-up

After creating your practice, Genkō shows a one-time Secure your account prompt where you can enroll a TOTP app or a passkey before proceeding to the dashboard. This step is optional — you can skip it and set up 2FA later from Settings → Security.

Ready to get started?

Sign up for free — no credit card required.