Build a Hospital Appointment API

Build a Frappe app with a secure, privacy-conscious appointment integration API.

Introduction

30 Second Summary

A hospital schedule is only useful when every booking connects the right patient with the right practitioner. Another system may need that schedule without needing the patient's personal details.

In this project, you will build a hospital appointment hub with Frappe Framework version 15. You will also create a secure REST API that gives an external service a read-only feed of approved appointment details.

What You'll Build

By the end, you'll schedule a dummy future appointment in Frappe Desk and retrieve its approved details through an authenticated request that keeps patient profile data out of the response.

By the end of this project, you'll have:

  • A linked appointment workflow where every dummy booking connects an HT Patient with an HT Practitioner.
  • Server-side validation that rejects a past booking with Appointment date cannot be in the past.
  • A least-privilege appointment feed you can call with token authentication to see only approved appointment details.
  • Secret Mission: Add a validated status filter that accepts approved appointment states and rejects unsupported values.

Are there any prerequisites?

This project requires an existing local Bench site running Frappe Framework version 15. You also need Git plus Visual Studio Code.

You need permission to sign in as an Administrator or a System Manager. This project uses your existing local site and adds no incremental service cost.

Before We Start

Before any hands-on work, commit to the hospital workflow plus its privacy boundary. The hub will connect dummy patient, practitioner, and appointment records so an external service can read approved scheduling data without patient names, dates of birth, or contact details.

Prepare the Training App

The dummy-data boundary is set. Your next risk is modifying the inherited project while preparing the exercise.

A separate Frappe Framework app keeps the training work isolated. Its source files can stay under Git control without changing the project you inherited.

In this step, get ready to:
  • Verify the inherited Bench environment.
  • Enable developer mode for repository-backed DocTypes.
  • Create the hospital_training app on the selected site.
Verify the inherited environment

Your local Bench can contain several sites or apps. A short preflight confirms that every later command targets the intended environment.

  • Use the Terminal window for your existing local Bench.
  • Move into the local Bench directory that you already use.
  • Confirm that the current directory belongs to a Bench by running:
bench find .

What does this check?

This command verifies that the current directory belongs to a Bench. It protects the next commands from running in an unrelated folder.

You should see the current Bench identified. Stay in this directory for the rest of the step.

Bench directory not found?

  • Return to the local Bench directory from your existing setup.
  • Repeat the directory check from there.

Ask for help locating the correct folder if the check still fails.

  • Check the versions of the installed apps by running:
bench version

What does the version check show?

The output lists the installed apps with their versions. The frappe line must begin with major version 15 for this project.

✔️ Frappe begins with version 15

Good. The inherited environment uses the required Frappe major version.

ⓧ Frappe shows another major version

This project must use the inherited Frappe v15 environment. Do not upgrade or downgrade this Bench.

  • Stop this project in the current Bench.
  • Return to the local Bench that runs Frappe major version 15.
  • Ask the environment owner which Bench you should use if the correct one is unclear.

ⓧ Bench command is unavailable

The inherited Bench environment is not active in this terminal session.

  • Return to the terminal setup that you normally use for the local Frappe site.
  • Move into the existing Bench directory.
  • Repeat the version check there.
  • List the apps across the local sites by running:
bench --site all list-apps -f text

Why list every site?

A Bench can host more than one local site. This list helps you choose the site that receives the training app.

  • Choose the local site that will hold the exercise.
  • Record its name here: <site>.
  • Use the saved site name whenever a command below shows <site>.
  • Confirm that curl is available by running:
curl --version

What does this confirm?

The command prints the installed curl version information. Later steps use this tool to call the integration API.

You should see curl version details in the terminal. That confirms the API testing tool is ready.

Terminal cannot find curl?

  • Pause before continuing with the project.
  • Restore curl through the setup process used for this inherited Mac.

Ask for help checking why curl is unavailable in this terminal.

Enable source generation

Developer mode makes standard DocTypes write their definitions into the app repository. This lets later schema changes become reviewable source files.

  • Enable global developer_mode by running:
bench set-config -g developer_mode 1

Why enable developer mode?

Developer mode lets Frappe generate repository-backed files for standard DocTypes. Those files make the hospital schema visible to version control.

  • Apply the configuration to <site> by clearing its cache with:
bench --site <site> clear-cache

Why clear the site cache?

The selected site may still hold the previous configuration in memory. Clearing its cache makes the developer mode setting available to later Desk actions.

The local Desk needs the development server to be available. Choose the state that matches your site.

✔️ Desk is already available

The existing development server is ready.

  • Keep its current terminal process running.

ⓧ Desk does not load

The development server occupies its terminal while it runs. Use a separate window so the current terminal remains available for app commands.

  • Use macOS search to open a separate Terminal window.
  • Return to your existing local Bench directory in the new window.
  • Start the development server by running:
bench start

What does this command do?

The command starts the local Bench services used by the development site. It keeps running while it prints service activity.

You should see service logs continue in the new window. Keep that window running for the rest of the project.

Development server does not start?

  • Confirm that the new terminal is inside the same local Bench directory.
  • Check the first reported failure for the service that could not start.

Ask for help interpreting the Bench startup output.

  • Navigate to the local site URL for <site> in your browser.
  • Sign in with an Administrator account or a user assigned the System Manager role.
  • Confirm that Desk loads after authentication.

You now have the access required to create repository-backed DocTypes in later steps.

Create and install the training app

A custom app gives the hospital exercise its own source boundary. The scaffold holds the module files that Frappe generates as the workflow grows.

  • Expect the app command to request project metadata.
  • Keep Hospital Training as the default title.
  • Use non-secret local training details for the remaining prompts.
  • Create the hospital_training app by running:
bench new-app hospital_training

What does app creation add?

The command scaffolds the app under apps/hospital_training. It creates the source structure that Git can track.

The app is still separate from every site at this point. Installation attaches it to the selected site.

When the command finishes, you should see the new app scaffold under apps/hospital_training.

App creation did not finish?

  • Check that you kept hospital_training as the app name.
  • Review the prompt where the command stopped.
  • Use non-secret values for any missing metadata.

Ask for help with the failed scaffold command.

  • Install hospital_training on <site> by running:
bench --site <site> install-app hospital_training

What does app installation change?

Installation attaches the app to the selected site. Frappe can now load its modules through that site.

You should return to the terminal prompt without an installation failure. The app is now available to the selected site.

App installation failed?

  • Confirm that <site> matches the selected local site exactly.
  • Confirm that app creation completed before retrying the installation.

Ask for help with the installation output if the app still cannot be attached.

Before the final checks, which app names do you expect to see? Which Frappe major version should still appear?

  • Confirm the installed apps on <site> by running:
bench --site <site> list-apps -f text

What proves the installation worked?

The site-specific list shows which apps Frappe can load for the selected site. Both frappe and hospital_training should appear.

You should see frappe plus hospital_training in the output.

  • Recheck the inherited app versions by running:
bench version

What does the final version check prove?

The final check confirms that creating the app did not change the inherited Frappe major version. The frappe line should still begin with version 15.

That foundation is now locked in. Your isolated training app is installed on the intended Frappe v15 site.

Your training boundary is ready. Next, you will model the linked hospital records inside this app.

Model the Appointment Workflow

Your version-controlled hospital_training app is installed. You can now turn it into a working hospital workflow inside Frappe Desk.

An appointment feed needs linked master records before it can represent real scheduling. You will model that workflow with three DocTypes. Server-side validation will reject past dates.

In this step, get ready to:
  • Define patient and practitioner records with stable identifiers.
  • Build appointments that link each patient to a practitioner.
  • Enforce the appointment date rule through the controller.
Create the patient and practitioner records

The patient and practitioner DocTypes hold reusable master data. Their ID fields become stable record names while their human-readable names appear in links.

  • Use the Awesomebar in Frappe Desk to open DocType List.
  • Select New.
  • Enter HT Patient as the DocType name.
  • Select Hospital Training as the module.
  • Leave Custom? unchecked so Frappe generates repository-backed source files.
  • Add a required unique Data field labeled Patient ID with fieldname patient_id. Show it in the list view.
  • Add a required Data field labeled Full Name with fieldname full_name. Show it in the list view.
  • Add an optional Date field labeled Date of Birth with fieldname date_of_birth.

Why name records by ID?

A patient ID stays stable when a person's display name changes. Using patient_id as the record name gives appointments a durable reference.

The title field keeps links readable by showing full_name to staff in Desk.

  • Configure the naming rule to use the patient_id field.
  • Enable record renaming.
  • Set full_name as the title field.
  • Enable the title field in link controls.
  • Set the sort field to modified in descending order.
  • Enable change tracking.
  • Add a System Manager permission row with Create, Delete, Email, Export, Print, Read, Report, Share, and Write enabled.
  • Select Save.

You should return to the DocType form with HT Patient saved in the Hospital Training module.

  • Use the Awesomebar to open the HT Patient list.
  • Select New.
  • Enter HT-PAT-001 in Patient ID.
  • Enter Jamie Rivera in Full Name.
  • Enter 1990-01-15 in Date of Birth.
  • Select Save.

Good progress. Your dummy patient is saved with HT-PAT-001 as its stable record name.

  • Return to DocType List through the Awesomebar.
  • Select New.
  • Enter HT Practitioner as the DocType name.
  • Select Hospital Training as the module.
  • Leave Custom? unchecked.
  • Add a required unique Data field labeled Practitioner ID with fieldname practitioner_id. Show it in the list view.
  • Add a required Data field labeled Practitioner Name with fieldname practitioner_name. Show it in the list view.
  • Add an optional Data field labeled Speciality with fieldname speciality.

How will appointments use this record?

The practitioner ID becomes the stored reference inside each appointment. The practitioner name remains the readable title that staff see when choosing a record.

  • Configure the naming rule to use the practitioner_id field.
  • Enable record renaming.
  • Set practitioner_name as the title field.
  • Enable the title field in link controls.
  • Set the sort field to modified in descending order.
  • Enable change tracking.
  • Add a System Manager permission row with Create, Delete, Email, Export, Print, Read, Report, Share, and Write enabled.
  • Select Save.

You should see HT Practitioner saved in the Hospital Training module.

  • Use the Awesomebar to open the HT Practitioner list.
  • Select New.
  • Enter HT-PRAC-001 in Practitioner ID.
  • Enter Casey Lee in Practitioner Name.
  • Enter General Medicine in Speciality.
  • Select Save.

Your two master records are ready. Their IDs now give the appointment model stable values to link.

Build the appointment DocType and controller

A Link field connects an appointment to an existing master record. This prevents staff from typing patient or practitioner identifiers that do not exist.

  • Return to DocType List through the Awesomebar.
  • Select New.
  • Enter HT Appointment as the DocType name.
  • Select Hospital Training as the module.
  • Leave Custom? unchecked.
  • Add a required Link field labeled Patient with fieldname patient pointing to HT Patient. Show it in the list view.
  • Add a required Link field labeled Practitioner with fieldname practitioner pointing to HT Practitioner. Show it in the list view.
  • Add a required Date field labeled Appointment Date with fieldname appointment_date. Show it in the list view.

What does the appointment model capture?

Each appointment stores references to one patient record and one practitioner record. The remaining fields capture when the appointment occurs plus its current status.

Those references become the identifiers that your integration feed can expose without returning patient profile data.

  • Add a required Time field labeled Appointment Time with fieldname appointment_time. Show it in the list view.
  • Add a required Select field labeled Status with fieldname status. Show it in the list view.
  • Enter the status options in this order: Scheduled, Checked In, Completed, and Cancelled.
  • Set Scheduled as the default status.
  • Set the sort field to appointment_date in ascending order.
  • Enable change tracking.
  • Add a System Manager permission row with Create, Delete, Email, Export, Print, Read, Report, Share, and Write enabled.
  • Select Save.

You should see the saved HT Appointment DocType with five fields. Its source files now live inside the installed app.

  • Press Cmd+Space on macOS or the Windows key on Windows to open system search.
  • Type Visual Studio Code and press Enter.
  • Select File from the top menu.
  • Select Open Folder.
  • Choose the local Bench directory from earlier.
  • Select Open.
  • Use the Explorer sidebar to open apps/hospital_training/hospital_training/hospital_training/doctype/ht_appointment/ht_appointment.py.
  • Replace the entire contents of ht_appointment.py with this controller code:
import frappe
from frappe.model.document import Document
from frappe.utils.data import getdate, nowdate


class HTAppointment(Document):
    def validate(self):
        if getdate(self.appointment_date) < getdate(nowdate()):
            frappe.throw("Appointment date cannot be in the past.")

What does this controller do?

  • The imports provide the document base class plus date helpers that compare the chosen date with today.
  • The validate() hook runs before Frappe saves an appointment.
  • The comparison converts both values into dates so time components cannot affect the rule.
  • The frappe.throw() call stops the save when appointment_date is earlier than today.
  • Save ht_appointment.py.
  • Confirm the editor shows validate() inside the HTAppointment class.

Cannot find the controller file?

Confirm that HT Appointment belongs to the Hospital Training module. Confirm that you left Custom? unchecked before saving the DocType.

A standard DocType generates its controller beneath the app's DocType directory. A custom DocType stores its definition only on the site.

Ask for help with finding the generated HT Appointment controller.

Your generated schema files plus the controller should now match these project artifacts.

✔️ Awesome, I've got everything!

Great. Save the generated DocType files plus ht_appointment.py before running the workflow.

ⓧ I'd like to double check the full code

Compare each generated file with the matching reference below.

apps/hospital_training/hospital_training/hospital_training/doctype/ht_patient/ht_patient.json.

{
  "actions": [],
  "allow_rename": 1,
  "autoname": "field:patient_id",
  "doctype": "DocType",
  "engine": "InnoDB",
  "field_order": [
    "patient_id",
    "full_name",
    "date_of_birth"
  ],
  "fields": [
    {
      "fieldname": "patient_id",
      "fieldtype": "Data",
      "in_list_view": 1,
      "label": "Patient ID",
      "reqd": 1,
      "unique": 1
    },
    {
      "fieldname": "full_name",
      "fieldtype": "Data",
      "in_list_view": 1,
      "label": "Full Name",
      "reqd": 1
    },
    {
      "fieldname": "date_of_birth",
      "fieldtype": "Date",
      "label": "Date of Birth"
    }
  ],
  "links": [],
  "module": "Hospital Training",
  "name": "HT Patient",
  "naming_rule": "By fieldname",
  "permissions": [{"create": 1, "delete": 1, "email": 1, "export": 1, "print": 1, "read": 1, "report": 1, "role": "System Manager", "share": 1, "write": 1}],
  "show_title_field_in_link": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
  "states": [],
  "title_field": "full_name",
  "track_changes": 1
}

apps/hospital_training/hospital_training/hospital_training/doctype/ht_patient/ht_patient.py.

from frappe.model.document import Document


class HTPatient(Document):
    pass

apps/hospital_training/hospital_training/hospital_training/doctype/ht_practitioner/ht_practitioner.json.

{
  "actions": [],
  "allow_rename": 1,
  "autoname": "field:practitioner_id",
  "doctype": "DocType",
  "engine": "InnoDB",
  "field_order": ["practitioner_id", "practitioner_name", "speciality"],
  "fields": [
    {"fieldname": "practitioner_id", "fieldtype": "Data", "in_list_view": 1, "label": "Practitioner ID", "reqd": 1, "unique": 1},
    {"fieldname": "practitioner_name", "fieldtype": "Data", "in_list_view": 1, "label": "Practitioner Name", "reqd": 1},
    {"fieldname": "speciality", "fieldtype": "Data", "label": "Speciality"}
  ],
  "links": [],
  "module": "Hospital Training",
  "name": "HT Practitioner",
  "naming_rule": "By fieldname",
  "permissions": [{"create": 1, "delete": 1, "email": 1, "export": 1, "print": 1, "read": 1, "report": 1, "role": "System Manager", "share": 1, "write": 1}],
  "show_title_field_in_link": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
  "states": [],
  "title_field": "practitioner_name",
  "track_changes": 1
}

apps/hospital_training/hospital_training/hospital_training/doctype/ht_practitioner/ht_practitioner.py.

from frappe.model.document import Document


class HTPractitioner(Document):
    pass

apps/hospital_training/hospital_training/hospital_training/doctype/ht_appointment/ht_appointment.json.

{
  "actions": [],
  "doctype": "DocType",
  "engine": "InnoDB",
  "field_order": ["patient", "practitioner", "appointment_date", "appointment_time", "status"],
  "fields": [
    {"fieldname": "patient", "fieldtype": "Link", "in_list_view": 1, "label": "Patient", "options": "HT Patient", "reqd": 1},
    {"fieldname": "practitioner", "fieldtype": "Link", "in_list_view": 1, "label": "Practitioner", "options": "HT Practitioner", "reqd": 1},
    {"fieldname": "appointment_date", "fieldtype": "Date", "in_list_view": 1, "label": "Appointment Date", "reqd": 1},
    {"fieldname": "appointment_time", "fieldtype": "Time", "in_list_view": 1, "label": "Appointment Time", "reqd": 1},
    {"default": "Scheduled", "fieldname": "status", "fieldtype": "Select", "in_list_view": 1, "label": "Status", "options": "Scheduled\nChecked In\nCompleted\nCancelled", "reqd": 1}
  ],
  "links": [],
  "module": "Hospital Training",
  "name": "HT Appointment",
  "permissions": [{"create": 1, "delete": 1, "email": 1, "export": 1, "print": 1, "read": 1, "report": 1, "role": "System Manager", "share": 1, "write": 1}],
  "sort_field": "appointment_date",
  "sort_order": "ASC",
  "states": [],
  "track_changes": 1
}

apps/hospital_training/hospital_training/hospital_training/doctype/ht_appointment/ht_appointment.py.

import frappe
from frappe.model.document import Document
from frappe.utils.data import getdate, nowdate


class HTAppointment(Document):
    def validate(self):
        if getdate(self.appointment_date) < getdate(nowdate()):
            frappe.throw("Appointment date cannot be in the past.")
Run the appointment workflow

The linked schema is ready. Saving real records now proves that the links resolve and the controller runs on the server.

  • Return to the Frappe Desk browser tab from earlier.
  • Reload the page so Desk uses the latest DocType definitions.
  • Use the Awesomebar to open the HT Appointment list.
  • Select New.
  • Select the patient record with ID HT-PAT-001.
  • Select the practitioner record with ID HT-PRAC-001.
  • Choose today's date or a later date in Appointment Date.
  • Enter a future time in Appointment Time.

The Status field should already show Scheduled because the DocType supplies that default.

  • Select Save.

That is the workflow running. Your linked appointment saves successfully with its patient, practitioner, date, time, and scheduled status.

The valid record proves the model works. A second record will test whether the server protects the schedule from past dates.

  • Select New from the HT Appointment list.
  • Select the patient record with ID HT-PAT-001.
  • Select the practitioner record with ID HT-PRAC-001.
  • Choose any date before today in Appointment Date.
  • Enter a time in Appointment Time.

Before you save, do you expect the server to accept this appointment or stop it?

  • Select Save.

You should see Appointment date cannot be in the past. The appointment remains unsaved because the controller rejected it.

That rejection is the intended result. It proves the rule runs during server-side validation instead of relying on the form alone.

  • Dismiss the validation message.
  • Return to the HT Appointment list.

You should see the valid appointment with Scheduled status. The past appointment should be absent from the list.

Past appointment saved successfully?

Confirm that you edited apps/hospital_training/hospital_training/hospital_training/doctype/ht_appointment/ht_appointment.py inside the installed app. Check that validate() is indented inside HTAppointment.

Reload Desk after saving the controller. Make sure the test date is earlier than today.

Ask for help with debugging the HT Appointment validation hook.

Your hospital workflow now has linked master data plus an enforced scheduling rule. Next, you will give a dedicated integration user read-only access and inspect Frappe's generated resource API.

Test the Generated Resource API

Your appointment workflow now rejects past dates. A valid scheduled appointment is ready for integration.

The Frappe Framework automatically exposes DocTypes through a generated REST API. A dedicated integration identity gives you a controlled way to test that API.

The key question is whether the generated response gives an external consumer a useful contract without tying that consumer to internal DocType details.

In this step, get ready to:
  • Establish a read-only integration identity for HT Appointment.
  • Generate API credentials for token authentication.
  • Inspect both generated resource response shapes.
Create a least-privilege integration user

Role-based permissions define what an identity can do after authentication. This integration identity needs read access to HT Appointment without access to Desk or record editing.

  • Use the Awesomebar at the top of Desk to search for Role.
  • Select Role from the search results.
  • Click New in the Role list.
  • Enter Hospital Integration Consumer in the Role Name field.
  • Clear Desk Access.
  • Click Save.

The role now exists without permission to enter Desk. Its next job is to carry one narrow data permission.

  • Use the Awesomebar to search for Role Permission Manager.
  • Open Role Permission Manager from the search results.
  • Select HT Appointment in the DocType selector.
  • Select Hospital Integration Consumer in the role selector.
  • Enable Read.
  • Confirm every permission except Read remains disabled.
  • Save the permission changes.

You should see HT Appointment configured with read-only access for Hospital Integration Consumer. The integration user can retrieve appointments without changing them.

Why use a dedicated role?

A dedicated role keeps the integration contract separate from human administrator access. You can change or revoke this API permission without affecting staff accounts.

The generated resource API checks the selected user's roles. Read-only access makes the permission boundary visible during every request.

  • Use the Awesomebar to search for User.
  • Open User from the search results.
  • Click New in the User list.
  • Enter hospital.integration@example.com as the dummy email address.
  • Enter Hospital Integration as the dummy first name.

These dummy identity fields distinguish the integration account from real hospital users. No real patient or staff information enters the exercise.

  • Select System User as the User Type.
  • Clear Send Welcome Email.

The account now has system access without sending mail to the dummy address.

  • Add Hospital Integration Consumer in the Roles table.
  • Click Save.

You should see Hospital Integration Consumer listed on the saved System User. The least-privilege identity is ready for authentication.

Generate credentials and verify the identity

Token authentication connects each API request to a specific Frappe user. The request inherits that user's roles and DocType permissions.

The next screen reveals the API Secret once. Store it safely because you need both credential values for this Terminal session.

  • Return to the integration User record from earlier.
  • Select the Settings tab.
  • Expand API Access.
  • Select Generate Keys.
  • Store the one-time API Secret in a secure password manager.
  • Store the API Key shown in the same section.

Your integration identity now has its own API credentials. Keep both values out of the hospital_training source files.

  • Record the local site origin from your browser address bar here: your local site URL.
  • Press Cmd+Space to open Spotlight on macOS.
  • Type Terminal into Spotlight.
  • Press Enter to open a new Terminal window.
  • Replace your-api-key-here with the generated API Key.
  • Replace your-api-secret-here with the one-time API Secret.
  • Set both values only in this Terminal session by running the edited commands:
export FRAPPE_URL="[[LOCAL_SITE_URL="your local site URL"]]"
export FRAPPE_TOKEN="your-api-key-here:your-api-secret-here"

Why use shell variables?

The export commands make the site URL and token available to commands in the current shell. Closing this Terminal session removes those values from the shell.

The token uses the API Key and API Secret separated by a colon. Keeping it in the shell prevents the credential from entering Git-tracked files.

Before you test the token, do you expect Frappe to identify the Administrator or the integration user?

  • Check the authenticated identity by running this request:
curl "$FRAPPE_URL/api/method/frappe.auth.get_logged_user" -H "Authorization: token $FRAPPE_TOKEN"

What does this request prove?

  • The method reports the user connected to the current authenticated request.
  • The authorization header supplies the API token stored in FRAPPE_TOKEN.
  • You should see hospital.integration@example.com under message.

That identity check is complete. Your API requests now run with the dedicated integration user's permissions.

Integration email missing?

Check that the API Key appears before the colon in FRAPPE_TOKEN. Check that the one-time API Secret appears after the colon.

Confirm the integration User remains enabled. Regenerate the credentials if the secret was copied incorrectly.

Help me troubleshoot token authentication for my Frappe integration user.

Inspect the generated resource contract

Frappe provides an automatic resource endpoint for each DocType. You will test its default response before deciding whether that contract gives an external consumer enough information.

Before you run the request, how much appointment information do you think the default endpoint returns?

  • Call the generated HT Appointment resource endpoint by running:
curl "$FRAPPE_URL/api/resource/HT%20Appointment" -H "Authorization: token $FRAPPE_TOKEN"

What does the default response contain?

You should see a JSON data array containing an object with only name. The request succeeds because the integration role has Read permission for HT Appointment.

The identifier proves the automatic resource endpoint works. It gives the consumer no appointment date, practitioner identifier, or status.

That shortfall is intentional. The automatic endpoint identifies the record without providing the appointment details an integration needs.

Before you request specific fields, do you think the response will create a consumer-focused contract or mirror the internal DocType schema?

  • Request the appointment fields explicitly by running:
curl --get "$FRAPPE_URL/api/resource/HT%20Appointment" -H "Authorization: token $FRAPPE_TOKEN" --data-urlencode 'fields=["name","patient","practitioner","appointment_date","appointment_time","status"]'

What changes in the response?

You should see the selected fields under the JSON data array. The response uses name, patient, practitioner, appointment_date, appointment_time, and status directly.

The field-selected request provides useful data through the internal DocType names. Any schema rename would force the external consumer to change its request or parsing logic.

You have now observed both sides of the generated resource API. It provides an immediate authenticated integration path while exposing a generic contract tied to the DocType schema.

Appointment data missing?

Confirm the valid Scheduled HT Appointment still appears in Desk. Check that Hospital Integration Consumer retains Read permission for HT Appointment.

If authentication works but the resource request fails, review the role assignment on the integration User. Avoid pasting the API token into any support message.

Help me troubleshoot my generated HT Appointment resource request.

✔️ Awesome, I've got everything!

Your integration user can authenticate and retrieve HT Appointment records. The controller from the previous step remains unchanged.

ⓧ I'd like to double check the full code

API permissions and credentials are site data in this step. Your existing appointment controller should still contain this exact code.

import frappe
from frappe.model.document import Document
from frappe.utils.data import getdate, nowdate


class HTAppointment(Document):
    def validate(self):
        if getdate(self.appointment_date) < getdate(nowdate()):
            frappe.throw("Appointment date cannot be in the past.")

This controller remains unchanged during API testing. It still prevents past appointments from being saved.

Your read-only integration identity can now reach Frappe's generated resource API. Next, you will replace its schema-dependent response with a curated appointment feed.

Build the Appointment Feed

Your integration user can read HT Appointment records through Frappe's generated resource API. That response still ties the consumer to internal DocType field names.

A purpose-specific feed gives the external consumer a stable contract. You will build a GET-only whitelisted method with permission-aware filtering.

In this step, get ready to:
  • Create a GET-only whitelisted method with an explicit role gate.
  • Query upcoming non-cancelled appointments through permission-aware list access.
  • Verify the curated response contract without exposing patient profile data.
Create the curated feed

Frappe whitelisted methods expose named Python functions through HTTP. The decorator can restrict this feed to GET requests.

  • Use Visual Studio Code to open the existing Bench directory.
  • Use the file controls in the left sidebar to create api.py at apps/hospital_training/hospital_training/api.py.
  • Add the imports, access controls, and appointment query by pasting this code into api.py:
import frappe
from frappe.utils.data import nowdate


@frappe.whitelist(methods=["GET"])
def get_appointment_feed():
    frappe.only_for("Hospital Integration Consumer")

    appointments = frappe.get_list(
        "HT Appointment",
        filters={
            "appointment_date": [">=", nowdate()],
            "status": ["!=", "Cancelled"],
        },
        fields=[
            "name",
            "patient",
            "practitioner",
            "appointment_date",
            "appointment_time",
            "status",
        ],
        order_by="appointment_date asc",
        limit_page_length=20,
    )

What does this code do?

  • The @frappe.whitelist(methods=["GET"]) decorator exposes the function through Frappe's method API.
  • The frappe.only_for("Hospital Integration Consumer") call requires the dedicated integration role.
  • The frappe.get_list query applies the session user's DocType permissions.
  • The date filter keeps appointments scheduled for today or later.
  • The status filter excludes records marked Cancelled.
  • The field list limits the internal data read by the method.
  • The query sorts appointments by date.
  • The page limit caps the response at 20 records.
  • Save api.py.
  • Confirm api.py appears inside apps/hospital_training/hospital_training in Visual Studio Code.

You should see the new file at the same package level as the existing Hospital Training application files.

File in the wrong folder?

Check that api.py sits directly inside apps/hospital_training/hospital_training. A file inside the nested DocType module has a different Python import path.

Ask for help with the file location.

The query currently holds Frappe's internal appointment fields. The return mapping gives the external consumer a curated set of keys.

  • Complete get_appointment_feed() by appending this return mapping below the query:
    return [
        {
            "appointment_id": appointment["name"],
            "patient_id": appointment["patient"],
            "practitioner_id": appointment["practitioner"],
            "appointment_date": appointment["appointment_date"],
            "appointment_time": appointment["appointment_time"],
            "status": appointment["status"],
        }
        for appointment in appointments
    ]

Why map the response keys?

The mapping translates internal record fields into an integration contract. External consumers receive identifiers without patient profile data.

This boundary lets the internal DocType evolve without forcing consumers to understand every stored field.

  • Save api.py.
  • Confirm the return mapping stays indented inside get_appointment_feed().

You should see six curated output keys in the return mapping.

Seeing indentation problems?

Align return with the appointments assignment. Keep the dictionary indented inside the returned list.

Ask for help checking the function structure.

✔️ Awesome, I've got everything!

Your appointment feed file is complete. Keep the existing appointment controller unchanged.

ⓧ I'd like to double check the full code

Compare apps/hospital_training/hospital_training/api.py with this complete file.

import frappe
from frappe.utils.data import nowdate


@frappe.whitelist(methods=["GET"])
def get_appointment_feed():
    frappe.only_for("Hospital Integration Consumer")

    appointments = frappe.get_list(
        "HT Appointment",
        filters={
            "appointment_date": [">=", nowdate()],
            "status": ["!=", "Cancelled"],
        },
        fields=[
            "name",
            "patient",
            "practitioner",
            "appointment_date",
            "appointment_time",
            "status",
        ],
        order_by="appointment_date asc",
        limit_page_length=20,
    )

    return [
        {
            "appointment_id": appointment["name"],
            "patient_id": appointment["patient"],
            "practitioner_id": appointment["practitioner"],
            "appointment_date": appointment["appointment_date"],
            "appointment_time": appointment["appointment_time"],
            "status": appointment["status"],
        }
        for appointment in appointments
    ]

How should the feed file work?

The full file authenticates the caller through the existing session. It requires the integration role before running the permission-aware query.

The final mapping returns the approved contract under Frappe's method response wrapper.

Confirm the existing controller at apps/hospital_training/hospital_training/hospital_training/doctype/ht_appointment/ht_appointment.py still contains this code.

import frappe
from frappe.model.document import Document
from frappe.utils.data import getdate, nowdate


class HTAppointment(Document):
    def validate(self):
        if getdate(self.appointment_date) < getdate(nowdate()):
            frappe.throw("Appointment date cannot be in the past.")

Why keep the controller unchanged?

The controller continues to reject past appointments during document validation. The feed adds a read boundary without changing that existing business rule.

Inspect the source changes

Git shows which files differ from the app repository. This check helps you separate source changes from credentials stored in the current shell.

  • Switch back to the current Terminal in your existing Bench directory.
  • Inspect the Hospital Training app's source changes by running this command:
git -C apps/hospital_training status --short

What does this command check?

The -C option tells Git to inspect the repository inside apps/hospital_training. The short format keeps the changed-file list compact.

You should see api.py with the generated DocType source changes from earlier steps.

  • Review every path listed by Git in Visual Studio Code.
  • Confirm each changed file contains source code or DocType metadata.
  • Confirm no API key value appears in the changed files.
  • Confirm no API secret value appears in the changed files.

Your credentials should remain only in FRAPPE_TOKEN inside the current Terminal session.

Don't see api.py in Git status?

Confirm the command ran from the Bench directory. Check that api.py is inside the app repository at the expected path.

Ask for help locating the missing source change.

Call the custom method

Whitelisted method results appear under message. The curated keys separate the consumer from the generated resource API's data contract.

Before you call the method, consider whether its response will expose the same field names as the generated resource response.

  • Call the authenticated appointment feed by running this command:
curl "$FRAPPE_URL/api/method/hospital_training.api.get_appointment_feed" -H "Authorization: token $FRAPPE_TOKEN"

What does this request prove?

The curl request sends the token from your current shell. The credential value stays outside the project files.

The message array should contain:

  • The appointment_id key for the appointment record identifier.
  • The patient_id key for the linked patient identifier.
  • The practitioner_id key for the linked practitioner identifier.
  • The appointment_date key for the scheduled date.
  • The appointment_time key for the scheduled time.
  • The status key for the appointment state.

The role gate checks membership before the query runs. The permission-aware list query still enforces the integration user's read access.

  • Compare the custom message contract with the generated resource API's data contract.
  • Confirm no patient name appears in the custom response.
  • Confirm no birth date appears in the custom response.
  • Confirm no contact field appears in the custom response.
  • Confirm every returned appointment is dated today or later.
  • Confirm no returned appointment has the Cancelled status.

You have closed the contract gap with a stable appointment feed. Patient profile data stays outside the response.

Custom method request not working?

Confirm the current Terminal still has FRAPPE_URL and FRAPPE_TOKEN available. Confirm the integration user still has the Hospital Integration Consumer role.

Check that Role Permission Manager still grants Read permission for HT Appointment. The custom method depends on both access checks.

Ask for help debugging the authenticated request.

Secret mission

Add a Safe Status Filter

Give external consumers a safe way to request appointments with one specific status. The feed will accept known values while rejecting anything outside its published contract.

Clean Up Your Resources

Clean Up Your Resources

Your local Frappe v15 project adds no paid service cost. Decide whether to keep the resources available, pause your local access, or delete the training setup.

Resources you used:

  • The hospital_training app installed on your selected Frappe site.
  • The HT Patient, HT Practitioner, and HT Appointment DocTypes with their local tables and dummy records.
  • The integration System User with its API credentials.
  • The Hospital Integration Consumer Role with its HT Appointment Read permission.
  • The local apps/hospital_training source folder and its Git repository.
  • The current Terminal variables FRAPPE_URL and FRAPPE_TOKEN.

Keep everything running

No action is needed for billing. Your local app remains available for more demonstrations or development.

A fresh API secret keeps the demonstration credential from lingering after the project.

  • Select Settings in the integration User record.
  • Expand API Access.
  • Select Generate Keys.
  • Store the one-time API Secret safely.
  • Review the changed files in the apps/hospital_training repository before committing source code.
  • Confirm that no API key or API secret appears in a source file.
  • Clear the temporary authentication values from your current Terminal session by running:
unset FRAPPE_TOKEN FRAPPE_URL

What Gets Cleared?

This command removes FRAPPE_TOKEN and FRAPPE_URL from the current shell session. Your app and site data remain available.

Pause - I'll come back to this later

Pausing frees local resources while keeping the app data and source files ready for your return.

  • Stop bench start by pressing Control-C in its Terminal window.
  • Disable the integration User from its Frappe Desk record.
  • Clear the temporary authentication values from your current Terminal session by running:
unset FRAPPE_TOKEN FRAPPE_URL

What Stays Available?

The hospital_training app remains installed. Its DocTypes, dummy records, role, permissions, and source files stay in place.

Delete - I don't want to use this again

Removing a local app can feel drastic. The dry run gives you a preview before Frappe changes the site.

  • Stop bench start by pressing Control-C in its Terminal window.
  • Delete the integration User from its Frappe Desk record.
  • Return to the Bench directory in the Terminal from earlier.
  • Preview the DocTypes that Frappe plans to remove by running:
bench --site <site> uninstall-app hospital_training --dry-run

What Does the Dry Run Do?

The dry run lists the DocTypes that the uninstall would delete. Use this preview to check the scope before removing site data.

  • Review the dry-run output for the DocTypes Frappe plans to remove.
  • Confirm that HT Patient, HT Practitioner, and HT Appointment appear in the preview.

Ready to Uninstall?

The next command creates a backup before removing the app's linked DocTypes and modules from the selected site.

  • Uninstall the app from the selected site by running:
bench --site <site> uninstall-app hospital_training

What Does This Remove?

Frappe removes the app's linked DocTypes and modules from the selected site. The local app source remains in the Bench.

  • Delete the Hospital Integration Consumer Role from its Frappe Desk record.
  • Confirm that hospital_training is uninstalled from every site in this Bench.
  • Remove the local app source after confirming that no site uses it by running:
bench remove-app hospital_training

What Does App Removal Do?

This command removes the hospital_training app from the Bench entirely. The local apps/hospital_training source folder is removed with it.

  • Clear the temporary authentication values from your current Terminal session by running:
unset FRAPPE_TOKEN FRAPPE_URL

What Gets Cleared?

This removes the local URL and token values from the current shell session. The integration credential is already unusable because its User was deleted.

Nice Work!

Nice Work!

You did it! Your local Frappe Framework v15 hospital hub now turns linked appointment records into a secure integration feed.

You've learned how to:

  • Built a linked appointment workflow with DocTypes plus Link fields. Added server-side validation that rejects past appointments.
  • Configured a dedicated integration identity with token authentication. Limited its access through a dedicated role with read-only DocType permission.
  • Compared Frappe's generated resource API with a custom whitelisted method. Returned a curated appointment contract without patient profile data.
  • Completed the Secret Mission by adding a validated status filter. Unsupported values now return a clear validation error.

Ready to quiz yourself?