Cerbo API Integration Guide: Endpoints & Edge Cases

October 2, 2026
•
Need Help Marketing?
Software that medical practices need to have

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.

How the Cerbo API is built

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/

Authentication

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:

  • API credentials are issued by Cerbo and are separate from user logins. An API keypair can't log into the EHR, and an EHR login can't call the API.
  • They are server-side only. Never in browser JavaScript, a public form, or a mobile app bundle.
  • Permissions should be minimum-necessary. Ask for read-only or resource-restricted access where you can. Analytics use cases can enforce an anonymize flag that strips most identifiers.
  • You can lock the connection to a static IP if your server has one.
  • Ask for a sandbox before you build. Your first hundred test submissions should hit fake charts.
  • If credentials might be exposed, disable the API user in Cerbo immediately and request new ones.

Conventions that shape every build

  • Patient key: pt_id is an integer. Cast it explicitly if your middleware passes strings.
  • Pagination: use limit and offset, and check has_more in the response. Patient lists cap limit at 100.
  • Timestamps: responses return dates in UTC. Convert before you display or compare them.
  • Concurrency: Cerbo asks for single-threaded requests. Rate limits vary by endpoint and are tightest on credential-validation and email-sending routes.
  • Bulk syncs: use the delta endpoint instead of re-pulling everything, and schedule heavy jobs after 5 PM Pacific or before 8 AM Eastern.
  • Heavy endpoints: extended_details can exceed 1 MB per patient. Use targeted endpoints unless you truly need most of the chart.

pt_id is the spine

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.

Five automations worth building on the Cerbo API

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.

1. Intake forms that write the chart note

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.

2. Signed consent packets filed as real PDFs

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.

3. Two-way CRM sync from ad click to chart

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.

4. Custom portals, refill requests, and supplement reorders

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.

5. AI scribe notes posted for provider sign-off

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.

Endpoint reference and payload schemas

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.

  • Patients: GET /patients/search, POST /patients, PATCH /patients/{id}. Used to match or create charts from leads and intake.
  • Encounters: POST /encounters, GET /patients/{id}/encounters, GET /encounter_types. Used for intake notes and AI scribe notes.
  • Documents: POST /patients/{id}/documents, GET /patients/{id}/documents. Used for signed consents and outside records.
  • Appointments: GET and POST /appointments, PATCH /appointments/{id}, GET /appointments/availability. Used for custom booking and status sync to the CRM.
  • Patient tags: get, add or edit, and delete tags on a patient. Used to mirror CRM segments in the chart.
  • Vitals: weight, height, blood pressure, and custom vital readings. Used for weight-loss progress from a patient app.
  • Portal queue: supplements, prescriptions, refills, secure messages, and documents. Used for patient requests that need staff approval.
  • Deltas: 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.

Patient search and create

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"
}

Encounter notes: POST /encounters

{
 "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.
  • A successful create returns 201, not 200. Error handling that treats every non-200 as a failure will flag good writes.

Document uploads: POST /patients/{pt_id}/documents

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:

  1. base64_content must be raw Base64. Strip the data:application/pdf;base64, prefix that browser file readers and canvas exports add.
  2. 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.
  3. Names must be unique. Cerbo requires each document's reference name to be unique within a patient's chart, so date-stamp every title and filename.

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.

Getting data out: webhooks vs. the delta endpoint

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:

  • Setup: webhooks live under Admin > Manage > Integrations, and only Superadmins can edit them. You choose a URL and the events to subscribe to.
  • Transport: the receiving URL must be HTTPS on a fully qualified domain with a valid SSL certificate.
  • Timing: they fire in real time.
  • No retries: any delivery that doesn't get a 200 back is gone. If your middleware is down for an hour, so is that hour of events.
  • No signature: Cerbo doesn't sign webhook messages. Add a custom authorization header in the webhook config, verify it on every request, and allowlist your Cerbo server's IP if you can.
  • Thin payloads: webhook bodies carry less data than API responses. Treat each one as a notification, then fetch the full object from the API.
  • PHI inside: every webhook can contain protected health information, so your receiver is in HIPAA scope.

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.

HIPAA guardrails for Cerbo integrations

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:

  • Every vendor that touches PHI needs a BAA. That includes your middleware, CRM, AI scribe, and any logging or monitoring tool. If your middleware vendor won't sign one, self-host n8n on infrastructure you control.
  • Execution logs are PHI stores. n8n saves execution data, payloads included, unless you configure it not to. Stop saving successful executions or prune them on a short schedule.
  • The browser only talks to your middleware. Cerbo credentials never ship in client code. If your form posts straight from the browser to your middleware, your static web host never touches PHI at all.
  • Send the CRM the minimum necessary. Contact details and operational fields go to the CRM. The clinical narrative stays in Cerbo. Our GoHighLevel HIPAA setup guide covers the CRM side.
  • anonymize isn't true de-identification. Cerbo warns that questionnaire answers aren't scrubbed and enough data points can still identify a patient.
  • Retire API users you no longer need. Disable them in Cerbo the day an integration is decommissioned.

None of this is legal advice. Run your architecture past your compliance officer before go-live.

Cerbo API FAQ

How does authentication work for the Cerbo API?

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.

Why is pt_id required for almost every Cerbo API call?

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.

What payload does Cerbo's document upload endpoint expect?

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.

Why don't documents uploaded through the API appear in the chart's folders right away?

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.

How do you fix "Bad control character in string literal" errors when posting notes?

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.

Why does n8n send "filesystem-v2" instead of my file's Base64?

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.

How should browser intake forms handle CORS preflight errors?

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.

What's the difference between multipart/form-data and JSON uploads in Cerbo?

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.

Does Cerbo retry failed webhooks?

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.

How do you tell a prospective patient from an active one through the API?

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.

Build it, or have it built

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.

Sources

Your Practice Isn’t Generic. Your Marketing Shouldn't Be Either.

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.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.