Read more Articles
Keep up to date with medspa marketing strategies.

Cerbo's API is the difference between an EHR your team works around and an EHR that runs your practice's automations. This guide covers how the API is structured, the endpoints that matter for real workflows, the payloads they expect, and the edge cases that break builds once real patient data starts flowing through them.
Cerbo is built for direct primary care, functional and integrative medicine, hormone therapy, and other cash-based practices. Out of the box it handles charting, scheduling, labs, supplements, and billing. What it can't do on its own is connect to everything around the chart: the intake form on your website, the CRM your front desk works leads in, the consent packet that needs a real signature, and the AI scribe writing notes in another app.
This guide is written for three readers: developers and automation builders wiring Cerbo to other systems, practice managers scoping an integration project, and agencies connecting a marketing stack to a clinical one. If you're specifically connecting Cerbo to GoHighLevel, our Cerbo + GoHighLevel integration guide covers that build end to end. This post goes one layer deeper, into the API itself.
Cerbo runs a conventional REST API over HTTPS, but a handful of its conventions decide whether an integration survives production. Every request goes to your practice's own subdomain. Cerbo is built by MD HQ, which is why the API still lives on md-hq.com:
https://{subdomain}.md-hq.com/api/v1/
Cerbo uses HTTP Basic Authentication. Your middleware base64-encodes api_username:secret_key and sends it on every request:
Authorization: Basic <base64(api_username:secret_key)>
The rules around those credentials matter more than the header itself. According to Cerbo's API documentation:
anonymize flag that strips most identifiers.pt_id is an integer. Cast it explicitly if your middleware passes strings.limit and offset, and check has_more in the response. Patient lists cap limit at 100.extended_details can exceed 1 MB per patient. Use targeted endpoints unless you truly need most of the chart.Every clinical object in Cerbo hangs off the patient ID: encounters, documents, vitals, tags, supplements, prescriptions, charges, and appointments. That makes the first job of almost every workflow the same: resolve the pt_id before you write anything.
The pattern is search first, create only on a miss. Query GET /patients/search by email and date of birth, use the returned ID if there's a match, and POST /patients only if there isn't. We walk through that exact branch step by step in our Cerbo + GoHighLevel intake guide. Skip it and you'll spend your first month merging duplicate charts.
The best Cerbo integrations remove one manual handoff between a system patients touch and the chart. These are the five we see pay off most in cash-based practices, mapped to the endpoints that power them.
One rule before any of them: check Cerbo's native integrations first. Cerbo already connects to Fullscript for supplement plans (bidirectional), Hint for DPC memberships, and ActiveCampaign for pushing new patients to email. Custom builds should fill gaps, not rebuild what a toggle already does.
The problem: a new patient answers 60+ questions online, then someone retypes the answers into the chart before the first visit.
The build: the form posts to your middleware, the middleware resolves the pt_id, and POST /encounters writes a structured, plaintext note to the chart. Two details most builds miss: pull a valid encounter_type from GET /encounter_types instead of hardcoding one, and set owner to the reviewing provider. Otherwise the note belongs to the API user. The full n8n build is in our Cerbo + GoHighLevel guide.
The problem: HRT, IV therapy, and medical weight loss programs need multi-page consents with exact formatting and a real signature. A form builder gives you a flat email.
The build: render the documents and a signature canvas in the browser, flatten them to a PDF with a library like pdf-lib, and send the Base64 string to your middleware. The middleware posts it to POST /patients/{pt_id}/documents. Stamp the signed timestamp and consent version into the PDF itself so the audit trail travels with the file.
Check first: Cerbo's native patient forms already support online signatures. Build custom packets only when you need layouts or flows the native forms can't produce.
The problem: marketing works leads in GoHighLevel, HubSpot, or ActiveCampaign. Clinical lives in Cerbo. Nobody can say which campaign produced which patient.
The build: when a lead books or submits intake, search Cerbo and create the chart as a prospective patient ("inactive": "prospective") until they convert. Write the returned pt_id back to a CRM custom field so every later event can join on it. In the other direction, Cerbo webhooks notify your middleware of changes, the middleware fetches the full record, and the CRM updates tags and pipeline stages. Patient tags (POST on the patient tags endpoint) let you mirror CRM segments inside Cerbo, too.
Watch out: when you read a patient back, inactive comes back as a boolean that is true for prospective, inactive, and deceased patients alike. Cerbo's docs say to use patient_status_description to get the real status. A sync that keys off inactive will treat every new lead like a churned patient.
This is the plumbing behind speed-to-lead follow-up and LTV-based segmentation. If your practice runs OptiMantra instead, the same pattern applies; see our OptiMantra webhook tutorial.
The problem: the native portal handles records well, but it isn't built like a storefront for recurring supplement orders or a branded refill experience.
The build: a custom web app (React, Vue, or similar) backed by your own server. Patients sign in with their existing Cerbo portal credentials, which your server checks against POST /patients/portal/validate_credentials. The app reads active supplements and prescriptions, then submits supplement additions and refill requests through Cerbo's portal-queue endpoints, where staff approve them. Payments run through Stripe or your existing gateway.
Watch out: credential-validation endpoints carry the tightest rate limits. Validate once at sign-in and hold a server-side session. Don't re-validate on every page load.
The problem: ambient AI scribes produce a solid draft note in a separate app, and someone still copies it into the chart.
The build: once the provider approves the draft in the scribe, the middleware posts it to POST /encounters with owner set to that provider. If the visit already has an encounter, pass its ID as parent_encounter and the AI note files as a sub-note under the visit. The provider reviews and signs in Cerbo like any other note.
Watch out: GET /patients/{id}/encounters returns only signed notes by default. Pass signed_only=false if your integration needs to read back drafts it posted.
This is AI where it belongs in a practice: in the back office, with a licensed provider signing every word. We make the longer case in why AI everywhere is dangerous for healthcare practices. Whatever scribe you use, it needs a BAA before it hears a single visit.
Cerbo's API reaches well past patients and encounters. These are the resources practices automate most, with the calls behind each one. Paths are relative to https://{subdomain}.md-hq.com/api/v1.
GET /patients/search, POST /patients, PATCH /patients/{id}. Used to match or create charts from leads and intake.POST /encounters, GET /patients/{id}/encounters, GET /encounter_types. Used for intake notes and AI scribe notes.POST /patients/{id}/documents, GET /patients/{id}/documents. Used for signed consents and outside records.GET and POST /appointments, PATCH /appointments/{id}, GET /appointments/availability. Used for custom booking and status sync to the CRM.GET /delta/{resource_type}. Used for scheduled syncs and nightly reconciliation.Watch out: to cancel an appointment, PATCH its status to cancelled. DELETE /appointments/{id} destroys the record entirely. Availability queries also max out at a 90-day window.
GET /patients/search accepts first_name, last_name, email, dob, username, and timezone. Names and email accept % wildcards. Email matches a patient's primary or secondary address. Date of birth is exact match only, formatted YYYY-MM-DD.
POST /patients requires first_name, last_name, dob, and sex, where sex must be exactly M, F, or ?. For leads who haven't converted, create them as prospective:
{
"first_name": "Jane",
"last_name": "Doe",
"dob": "1990-05-12",
"sex": "F",
"email1": "jane@example.com",
"inactive": "prospective"
}
{
"pt_id": 5273,
"date_of_service": "2026-10-02",
"title": "New Patient Intake",
"content": "=== DEMOGRAPHICS ===\nName: Jane Doe\nDOB: 1990-05-12\n\n=== CHIEF CONCERNS & GOALS ===\n...",
"encounter_type": "ov",
"owner": 14
}
content is plaintext. HTML and Markdown won't render, so build readability with line breaks and section dividers.encounter_type is a short code such as ov for an office visit. Pull the valid list from GET /encounter_types.owner is the user responsible for the note. It defaults to the API user, which is rarely what the provider wants.parent_encounter (optional) files the note as a sub-note. The parent must belong to the same patient and can't be a sub-note itself.This is the endpoint behind consent packets, and the one with the most surprises. This payload is what has worked in our production builds:
{
"pt_id": 5273,
"name": "2026-10-02_Jane_Doe_HIPAA_Notice.pdf",
"title": "2026-10-02 Signed HIPAA Notice",
"description": "Signed HIPAA Notice of Privacy Practices",
"mime_type": "application/pdf",
"filename": "2026-10-02_Jane_Doe_HIPAA_Notice.pdf",
"base64_content": "JVBERi0xLjQKJ...",
"enqueue": true,
"notes": "Signed via online intake, consent version 2026.07"
}
Three things decide whether it works:
base64_content must be raw Base64. Strip the data:application/pdf;base64, prefix that browser file readers and canvas exports add.enqueue decides where the file lands. Per Cerbo's docs, it defaults to true, which sends the document to the Patient Portal Queue for staff review. notes adds context to that queue item. Set enqueue to false and pass folder (the name of an existing top-level document folder) plus an optional subfolder to file it directly.Our recommendation: queue anything clinical from an outside source, and auto-file routine signed consents into a dedicated folder once the practice approves that policy in writing.
Watch out: Cerbo's help docs cite a 16 MB upload cap, and Base64 encoding inflates a file by about a third. Compress scanned or image-heavy packets before encoding.
Use webhooks for anything time-sensitive and the delta endpoint for reconciliation, and run both, because Cerbo webhooks don't retry. Here's what Cerbo's documentation says about how they behave:
The delta endpoint covers the gaps. GET /delta/{resource_type}?start_date=...&end_date=... returns everything created, modified, or removed in a window of up to one week. Supported resources include patients, appointments, documents, pt_tags, supplements, orders, rxs, and tasks.
The production pattern we use: the webhook returns a 200 immediately, the middleware fetches the full record and processes it, and a nightly delta job catches anything that slipped through. For the general mechanics, see our healthcare webhooks guide and how to map webhook data into your CRM.
Cerbo operates under a BAA. Your integration is the part that usually doesn't. These are the gaps we find most often when auditing custom builds:
anonymize isn't true de-identification. Cerbo warns that questionnaire answers aren't scrubbed and enough data points can still identify a patient.None of this is legal advice. Run your architecture past your compliance officer before go-live.
Cerbo uses HTTP Basic Authentication over HTTPS. Cerbo issues an API username and secret key on request, and your server sends base64(username:secret) in an Authorization: Basic header. These credentials are separate from EHR logins and must never be used client-side.
pt_id is Cerbo's universal patient key. Encounters, documents, vitals, tags, and charges all attach to it, either in the URL path or the JSON body. Resolve it first with GET /patients/search, and create the patient only when the search returns no match.
POST /patients/{pt_id}/documents takes JSON, not multipart. In our builds, the working payload includes pt_id, name, title, description, mime_type, filename, and base64_content, with the Base64 string stripped of its data: prefix. Optional enqueue, notes, folder, and subfolder fields control where the file lands.
Because enqueue defaults to true. Cerbo routes the document to the Patient Portal Queue for staff review instead of filing it, so it sits as a pending request until someone accepts it. To file it directly, send "enqueue": false with the name of an existing document folder.
The error means raw line breaks, tabs, or quotes from patient answers broke your JSON string. Never build JSON by concatenating text. In n8n, define the HTTP body with "Using Fields Below" so n8n serializes it, or build the object in a Code node and let JSON.stringify handle the escaping.
Newer n8n setups store binary files on disk and leave a reference string, filesystem-v2, where the Base64 used to be. Cerbo rejects it. Read the real bytes with this.helpers.getBinaryDataBuffer(0, 'data') and convert with .toString('base64'), per n8n's docs, or reuse the Base64 string if the browser already sent one in the JSON payload.
Browsers send an OPTIONS preflight before cross-origin application/json POSTs, and many webhook endpoints don't answer it. The clean fix is configuring your endpoint to allow your form's origin. The workaround is sending the JSON string as a form-encoded field with URLSearchParams, which skips preflight. The request still arrives, but your page may not be able to read the response, so confirm success another way.
Cerbo's document endpoint expects application/json with the file as a Base64 string. In our testing, sending multipart/form-data returns a 500 error: "Invalid format - you must pass JSON formatted data." Encode the file, place it in base64_content, and send JSON.
No. Cerbo doesn't re-send webhooks that fail to get a 200 response, so events during an outage are lost. Return 200 immediately, process the event afterward, and run a scheduled job against GET /delta/{resource_type} to catch anything you missed.
Read patient_status_description, not inactive. Cerbo returns inactive: true for prospective, inactive, and deceased patients alike. patient_status_description returns the actual status: prospective, active, inactive, or deceased.
Cerbo's API is mature enough to run most of a cash-based practice's back office. The endpoints aren't the hard part. The edge cases are: the portal queue, the status field, the webhooks that never retry, and the PHI sitting in your execution logs. Get those right and the same pt_id-anchored pattern scales from one intake form to a full CRM and EHR automation stack.
NexaMed helps established cash-pay healthcare practices build the systems behind sustainable growth. We specialize in Cerbo and OptiMantra integrations, GoHighLevel architecture, CRM automation, SEO, and paid acquisition for medspas, longevity clinics, sexual wellness practices, and other private-pay healthcare organizations.
Whether you need a complex Cerbo or OptiMantra integration, a better patient acquisition system, or a marketing partner capable of connecting your ads, SEO, CRM, automations, and EHR into one cohesive operation, NexaMed is built for that level of work.
If you are looking for a partner that understands both healthcare marketing and the technical infrastructure behind it, book a discovery call with NexaMed.
You’ve outgrown "basic" marketing. Nexamed builds the advanced lead-gen infrastructure your med spa needs to capture high-ticket patients and scale without the manual mess.
Keep up to date with medspa marketing strategies.
.png)
