Healthcare Clinic Attribution Data Model
Healthcare Clinic Attribution Data Model
One clinic's patients followed from an anonymous ad click through enquiries, clinician-qualified consultations and treatment to the copay and insurer payment, on a single identity minted at the first page view.
A single healthcare practice — one clinic, one patient list — that advertises for patients, screens the enquiries that come back, treats the people a clinician accepts, and is paid in two parts: the patient's copay before treatment, and the insurer's share once the claim has been processed. The model follows that one chain end to end, from an anonymous ad click to money received.
What makes it more than a funnel report is the person identity at its centre. It is minted on someone's very first page view — before a name, an email or a phone number — and it is never reissued, so a patient paying today still resolves to the advertising that first brought them, whether that was weeks or years earlier.
That stitching — one identity held across devices, browsers and years — is the pattern used by APAS® Cloud, whose co-founder described it for this model.
Two distinctions inside it are expensive to lose. An enquiry is not a qualified enquiry: qualification is a clinician's decision, taken at a consultation, and the field is empty on the enquiry record until then — so a report that counts empty as "not qualified" misreads its own funnel. And an invoice is not revenue: a copay is money, a claim is a promise, and a practice that reads its bills as income believes it has been paid when it has not.
Scope: there is no advertising cost anywhere in this model. Clicks, visits, enquiries, consultations, treatments, bills and payments are all here; what the practice paid the ad platforms for them is not, so cost per acquisition, return on ad spend and budget allocation have no answer in it. What it does answer is which advertising brings enquiries a clinician will accept, and what those enquiries are worth once they have been treated and paid for. Also absent, and worth naming so that nobody looks for them: insurance claims as objects — only their trace on an invoice and a payment — multiple clinic locations, clinician capacity and scheduling, and a coded procedure catalogue.
Three healthcare bundles, and which is which. healthcare models a hospital system: appointments, clinical encounters, bed census and the insurance claims that pay for them. healthcare-clinic-network models a multi-location outpatient operation, with advertising spend, clinics, providers and no-shows. This one models a single practice, and its subject is the identity and attribution chain: one person, followed from the click that found them to the payment that settled their treatment.
Example Questions
- Which
pagesand which creatives bring the enquiries a clinician goes on to qualify, rather than the ones that bringclicksand stop there? - Which advertising brings enquiries that look right all the way to the
consultationand are then refused by the clinician, and what reason is recorded for the refusal? - How long does money take to arrive once a
treatmentis booked, split between the copay the patient pays before treatment and the insurer's reimbursement afterwards?
Call
Every telephone conversation between the practice and someone enquiring. A phone call and a web form are the same step in the funnel, not two funnels: both are the first engagement, both create the same CRM record, and both carry the same advertising identifiers when the caller was on the website. A report built on web forms alone leaves telephone enquiries out and credits the wrong channels.
Those identifiers are empty for a caller with no web history, and that is the record being accurate rather than a tracking defect. Someone rings a number from a leaflet, a sign or a recommendation, having never opened the site; there is no visit to match them to and no click to credit. Dropping unattributed calls makes the measurable channels look better than they are, so they are kept and reported as demand the practice knows it has and cannot yet trace.
direction separates calls received from calls placed: a first screen can begin with the practice ringing back, and treating a returned call as a fresh enquiry would double-count demand.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
call_id | STRING | Call ID | PK. Unique identifier for this one call, separate from the enquiry it belongs to — one enquiry can run to several calls. |
lead_id | STRING | Lead ID | The enquiry record this call opened or added to; every later call about the same enquiry carries the same value. FK to Lead |
person_id | STRING | Person ID | The human on the other end, where they can be recognised from the website — reachable through the enquiry this call opened or through the visit it was placed from. Empty for a caller never seen online, which is a fact about the call rather than missing data. |
session_id | STRING | Session ID | The visit the call was placed from, where there was one. Empty for a caller who dialled a number they saw somewhere other than the site. FK to Session |
click_id | STRING | Click ID | The paid click behind the call. Empty for organic callers and for anyone who never visited the site at all. FK to Click |
called_at | TIMESTAMP | Called At | When the call took place. Compared with the enquiry's creation time, it separates the first contact from the follow-ups. |
direction | STRING | Direction | Who placed it: inbound when the person rang the practice, outbound when reception rang them. A returned call is not a second enquiry. |
duration_seconds | INTEGER | Call Length (Seconds) | How long the conversation lasted, in seconds — the difference between a call that was merely picked up and one in which the practice actually spoke to someone. |
answered | BOOLEAN | Answered | Whether anyone picked up. A call nobody answered is an enquiry that never reached a person, and it appears in no report built on web forms. |
tracking_number | STRING | Tracking Number | The number the caller dialled. When a practice publishes a different number per place — site, ad, print — this is what attributes a call that has no web history at all. |
utm_source | STRING | Call Source | Source declared by the link that brought this caller to the site, where there was one, such as google or facebook. Empty for a caller with no web visit behind them. |
utm_medium | STRING | Call Medium | Medium declared by that link, such as cpc or paid_social — the tag that marks this call as bought rather than earned. |
utm_campaign | STRING | Call Campaign | Campaign the call is credited to, as named in the ad account. Empty both for unattributed callers and for arrivals that carry no campaign. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Click | click_id = click_id | N:1 | The paid click behind the call; absent for organic and offline callers. |
| Lead | lead_id = lead_id | N:1 | The CRM record this call produced or added to. |
| Session | session_id = session_id | N:1 | The visit the call was placed from; absent for a caller with no web history. |
Click
Every click on one of the practice's ads, held as its own record rather than as columns on whatever happened next, and keyed the same way on every platform so clicks across Google, Meta, Microsoft and TikTok count in one place. session_id is empty when the click produced no visit — the person tapped and went straight back, the page failed, tracking was blocked — and those clicks were still paid for, so dropping them is how advertising quietly looks better than it was.
platform_click_id holds the raw value the ad platform itself attached: the gclid, fbclid, msclkid or ttclid. It is the harder evidence of a paid arrival, and needs one guard: a value shorter than ten characters should be treated as absent — below that what turns up is undefined, null, empty strings and truncated fragments, which inflate whichever channel they land in. Lesley van de Mortel, who described this model, uses that minimum in her own SQL and pairs it with a check on utm_source and utm_medium, so a claimed channel agrees with itself from two directions.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
click_id | STRING | Click ID | PK. The practice's own identifier for the click, minted the same way on every platform so clicks can be counted in one place. |
person_id | STRING | Person ID | The human who clicked, which is what lets several clicks over months be recognised as one person's journey rather than several prospects. FK to Person |
session_id | STRING | Session ID | The visit the click produced. Empty when it produced none — the click was still paid for, and excluding those flatters the channel. FK to Session |
clicked_at | TIMESTAMP | Clicked At | When the ad was clicked. The gap between here and any money that follows can run from weeks to years in this business, so a same-month comparison will miss it. |
ad_platform | STRING | Ad Platform | Which advertising platform the click came from: google_ads, meta, microsoft or tiktok. |
platform_click_id | STRING | Platform Click ID | The raw identifier the platform attached — gclid, fbclid, msclkid or ttclid. Treat anything shorter than ten characters as absent: below that it is placeholder text, not a click. |
utm_source | STRING | UTM Source | Source the ad link declared, such as google or tiktok. Worth checking against ad_platform rather than trusting alone, because it is set by hand in the ad. |
utm_medium | STRING | UTM Medium | Medium the ad link declared, such as cpc or paid_social — the tag that marks this traffic as bought. |
utm_campaign | STRING | UTM Campaign | Campaign the click was bought under, as named in the ad account. |
utm_term | STRING | UTM Term | Keyword the click was bought under, where the platform works that way. Empty on platforms that sell audiences rather than search terms. |
utm_content | STRING | Ad Creative | Which creative was clicked — the image, video or headline. This is the field that makes "which ad works" answerable at all, rather than only "which campaign works". |
landing_page_url | STRING | Ad Landing Page | Address the ad sent the person to, which is what the click's promise has to be judged against. This is the ad's side of a pair: the visit records its own landing page separately. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Person | person_id = person_id | N:1 | The human who clicked the ad. |
| Session | session_id = session_id | N:1 | The visit the click landed in; absent when the click never produced one. |
Consultation
Where an enquiry is judged. A practice screens in two steps, both held here, told apart by type and by who conducted them: they differ in who screens, not in what is recorded. A Pre-Consultation is a telephone conversation held by a receptionist or a practice manager — not a clinician — who decides whether the practice is a plausible fit. A Consultation is the appointment that follows: a clinician examines the person and decides whether treatment can go ahead. The first step exists because the second is expensive: without it every enquiry would go straight to a clinician, and no practice has the hours for that.
Qualification is decided here, and only here. The enquiry record carries a field of the same name, but it is empty: at the moment of a form or a call nobody has yet formed a view. One enquiry can be screened more than once, so it may have several rows here, to be read in order. disqualification_reason carries the practice's own account of a refusal, which turns a low acceptance rate from a count into a diagnosis and shows which campaigns bring people the practice can actually treat.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
consultation_id | STRING | Consultation ID | PK. Unique identifier for this one screening, not for the enquiry — an enquiry can be screened more than once. |
lead_id | STRING | Lead ID | The enquiry being screened. Several rows here share one value, which is what makes this a sequence of steps rather than a single verdict. FK to Lead |
employee_id | STRING | Employee ID | Who conducted it. This is what distinguishes a receptionist's phone screen from a clinician's assessment in practice, and it lets acceptance rates be compared between staff. FK to Employee |
consultation_date | DATE | Consultation Date | The day it took place. Measured against the enquiry's creation time, it is how long the practice took to respond; measured against the previous screen, how long the person waited between steps. |
type | STRING | Consultation Type | Which of the two screens this is: Pre-Consultation for the phone conversation with reception or a manager, Consultation for the appointment with a clinician. |
is_qualified | BOOLEAN | Is Qualified | The verdict of this screen: TRUE when the person moves forward, FALSE when they do not. This is where qualification is actually decided; the same field on the enquiry record is empty. |
disqualification_reason | STRING | Disqualification Reason | Why the person was not taken forward. Empty when they were. This is what turns a low acceptance rate from a count into a diagnosis, because the fix is different for each reason. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Employee | employee_id = employee_id | N:1 | Who held it, which is what separates a phone screen from a clinician's assessment. |
| Lead | lead_id = lead_id | N:1 | The enquiry being screened; one lead can have several screenings. |
Customer (Patient)
The patient once money is involved. A person exists from the first anonymous visit; a customer exists once there is an invoice or revenue against them — not when someone enquires, and not when a clinician agrees to treat them — and that is the line between an audience and a patient list. In healthcare the two are the same human, so this is one record rather than two: customer_id is this model's key and patient_id the clinical system's, so a commercial report and a clinical record reconcile without renaming anything. That is not universal — elsewhere an employer or a school pays for the people being served, and payer and patient are separate records.
One customer covers several treatments and several payments, so counting rows here counts patients while counting treatments, invoices or payments counts events, and with returning patients the two are not the same. person_id resolves a paying patient back to the anonymous first visit, so the advertising that started a journey can be credited with the money that ended it, however long the gap.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
customer_id | STRING | Customer ID | PK. Unique identifier for the patient as a paying party, issued when the first invoice or payment appears rather than at the enquiry. |
person_id | STRING | Person ID | The same human at their anonymous stage, which is what ties money received back to the visit and the advertising that started the journey. FK to Person |
patient_id | STRING | Patient ID | The identifier the clinical system issues for the same human. Held alongside the commercial key so a clinical record and a billing record can be reconciled without renaming either. |
first_name | STRING | Patient First Name | Given name as the practice bills it. The enquiry record holds its own copy of the contact details, on Lead. |
last_name | STRING | Patient Last Name | Family name as the practice bills it. |
became_customer_at | TIMESTAMP | Became a Customer At | When this human stopped being an enquiry and became a paying patient: the moment of the first invoice or payment. Compared with the person's first visit, it is the full length of the journey from first touch to first money. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Person | person_id = person_id | N:1 | The same human, tracked from their first anonymous visit onward. |
Employee
Everyone at the practice who takes part in turning an enquiry into a booked treatment — clinicians, receptionists and practice managers alike — kept as one list and told apart by type: Doctor, Receptionist, Manager. They are one mart rather than three because they carry the same properties and differ only in what they do; splitting them would duplicate the same five fields three times and make every question about staff a question asked three times.
Both screening steps are staffed from here: the phone call that decides whether an enquiry is worth an appointment, and the appointment itself, where a clinician decides whether treatment can go ahead. Because both consultations point at this one mart, the same question — who held it, and how did it end — answers for either step, which is what lets a practice see where its enquiries are actually lost, and to whom. Without it every enquiry would appear to go straight to the doctor, and the screen that turns away a poor fit would be invisible.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
employee_id | STRING | Employee ID | PK. Unique identifier for this member of staff. |
type | STRING | Employee Type | What the person does in the funnel: Doctor sees patients in the room and decides whether treatment goes ahead, while Receptionist and Manager take the first screening call. |
first_name | STRING | First Name | Given name, as the practice records it. |
last_name | STRING | Last Name | Family name, as the practice records it. |
position | STRING | Position | Job title at the practice — finer than type, and the wording a rota or a staff list would use. |
specialty | STRING | Specialty | Clinical field the person practises in. Empty for reception and management staff, for whom it has no meaning. |
Form Submission
Every web form the practice receives, with the advertising that produced it attached. A form submission is one of the two ways an enquiry begins; ringing the practice is the other, and the two are the same step in the funnel. A submission opens a CRM record if the person has not enquired before and attaches to the existing one if they have, so one enquiry can produce several rows: counting rows counts contact attempts, not prospective patients.
It carries two families of identifiers worth keeping apart. The five utm_* fields are what the link itself declared, set by hand when the ad was built; the four click identifiers — gclid, fbclid, msclkid, ttclid — are what the advertising platform attached, and they are the harder evidence of the two. As on the ad click, a platform click identifier shorter than ten characters should be treated as absent.
An empty attribution field is normal rather than broken: someone who arrived through unpaid search or typed the address in carries no campaign and no click identifier, and the identifiers of platforms a person did not come through are empty too.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
form_submission_id | STRING | Form Submission ID | PK. Unique identifier for this one form, separate from the enquiry it belongs to — one enquiry can produce several. |
lead_id | STRING | Lead ID | The enquiry record this submission opened or added to; repeat submissions from the same person all carry the same value. FK to Lead |
person_id | STRING | Person ID | The human who submitted it. Reach them through the enquiry this submission opened or through the visit it was sent during, both of which carry the same identifier back to the anonymous browsing and advertising that came before. |
session_id | STRING | Session ID | The visit the form was sent during, which is how the pages read beforehand can be counted as part of the same decision. FK to Session |
click_id | STRING | Click ID | The paid click that brought this person to the site. Empty for organic and direct arrivals, which is the normal state rather than missing data. FK to Click |
submitted_at | TIMESTAMP | Submitted At | When the form was sent. Measured back to the person's first page view, it is how long the journey to an enquiry took. |
form_name | STRING | Form Name | Which form on the site was used — a consultation request asks for more commitment than a callback request, and they should not be counted as one. |
page_url | STRING | Form Page | Address of the page the form was sent from, which is the page whose argument actually persuaded the person to get in touch. |
utm_source | STRING | Submission Source | Source declared by the link that brought this person, such as google or tiktok. Set by hand in the ad, so worth checking against the platform's own click identifier. |
utm_medium | STRING | Submission Medium | Medium declared by the link, such as cpc or paid_social — the tag that marks this enquiry as bought rather than earned. |
utm_campaign | STRING | Submission Campaign | Campaign the enquiry is credited to, as named in the ad account. Empty for arrivals that carry no campaign at all. |
utm_term | STRING | Submission Search Term | Keyword behind the enquiry, where the channel sells keywords. Empty on platforms that sell audiences instead. |
utm_content | STRING | Submission Creative | Which creative the person came through — the image, video or headline. This is the level at which advertising is actually changed, so it is the level worth judging enquiries at. |
gclid | STRING | Google Click ID | Google's own identifier for the click behind this submission. Treat a value shorter than ten characters as absent. |
fbclid | STRING | Meta Click ID | Meta's own identifier for the click behind this submission, covering Facebook and Instagram. Treat a value shorter than ten characters as absent. |
msclkid | STRING | Microsoft Click ID | Microsoft's own identifier for the click behind this submission. Treat a value shorter than ten characters as absent. |
ttclid | STRING | TikTok Click ID | TikTok's own identifier for the click behind this submission. Treat a value shorter than ten characters as absent. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Click | click_id = click_id | N:1 | The paid click that brought them; absent for organic and direct visits. |
| Lead | lead_id = lead_id | N:1 | The CRM record this submission produced or added to; one lead may submit several forms. |
| Session | session_id = session_id | N:1 | The visit it was submitted during. |
Invoice
A bill the practice has issued, raised against one treatment and one patient. An invoice is a claim on money, not money: it records what is owed and to whom, and nothing here says any of it has arrived. Money actually received is recorded on Revenue, joined by invoice_id: a report on invoices answers "what have we billed", one on revenue "what have we been paid". Read as income they overstate, carrying the full value of work billed, including the part an insurer has not settled and the part that may never be paid at all.
A treatment is billed in two parts. In the United States, where this pattern was described, the patient's copay can be billed as soon as the treatment is booked, is collected before it starts, and is money straight away, though a small share of the total. The rest is claimed from the insurer after the treatment and becomes money only when the insurer pays, with a lag. payer_type tells the two apart. A written_off invoice is delivered work that earned nothing, so it belongs in any honest reading of what a channel produced.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
invoice_id | STRING | Invoice ID | PK. Unique identifier for one bill. The patient's part and the insurer's part are billed separately, so one treatment can produce more than one row here. |
customer_id | STRING | Customer ID | The patient being billed. In healthcare this is the same human being treated, so no separate payer record is needed. FK to Customer (Patient) |
treatment_id | STRING | Treatment ID | The treatment this bill is for, which is how a charge connects back to the clinician's decision and, through it, to the advertising that produced the enquiry. FK to Treatment |
issued_at | TIMESTAMP | Issued At | When the bill was sent. For a copay this can be as early as the booking; for an insurance claim it follows the treatment. It is the start of the clock that Days to Payment measures. |
amount | NUMERIC | Invoiced Amount | What this bill asks for. It is not income — the amount actually received against it is recorded on Revenue, and it can be smaller, arrive in pieces, or never arrive. |
currency | STRING | Invoice Currency | Currency the bill was raised in, such as USD. Needed before amounts from different bills can be added together. |
payer_type | STRING | Payer Type | Who is being asked to pay: patient_copay for the part the patient settles before treatment, insurance_claim for the part claimed from the insurer afterwards. One is settled before the treatment, the other after the claim is processed. |
status | STRING | Invoice Status | Where the claim stands: issued, partially_paid, paid, or written_off when the practice has given up on it. Written-off bills are delivered work that earned nothing and should not be dropped from a channel's result. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Customer (Patient) | customer_id = customer_id | N:1 | Who is being billed. |
| Treatment | treatment_id = treatment_id | N:1 | What is being billed for. |
Lead
The enquiry as the practice's CRM holds it: a named person with contact details, created the moment someone first gets in touch, by web form or by phone. It does not exist beforehand, which is why the form submission and the call point at the lead rather than the other way round. person_id traces the enquiry back to the browsing that preceded it by months or years, and the advertising identifiers copied onto the record keep a campaign attached to that name long afterwards. One lead is one person's enquiry, not one contact attempt: the same lead may submit several forms, make several calls, and be screened more than once.
is_qualified is empty on this mart, and that is by design, not by omission. The CRM carries the field from the start, but at the enquiry stage nobody has yet spoken to the person; the decision is taken later, at a consultation, and stored there. A report that counts unqualified enquiries as is_qualified = false will therefore undercount them: the enquiries that were never accepted are not false, they are empty. To judge an enquiry's outcome, look at the consultations attached to it, not at this field.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
lead_id | STRING | Lead ID | PK. The CRM's own reference for the enquiry, issued when the first form or call arrives. It is a number in the CRM, carried here as a fixed-width string of digits so that sorting by it always follows the order the records were issued. |
person_id | STRING | Person ID | The human this enquiry belongs to, which is what ties a named enquiry back to the anonymous browsing and advertising that preceded it. FK to Person |
first_name | STRING | Lead First Name | Given name as the enquirer supplied it, which is what the practice has to work with — it need not match any later clinical record. |
last_name | STRING | Lead Last Name | Family name as the enquirer supplied it. |
email | STRING | Email address given at the enquiry. Empty when the person only ever telephoned and left no address. | |
phone | STRING | Phone | Telephone number given at the enquiry, and the one reception will call back on for the first screen. |
crm_contact_id | STRING | CRM Contact ID | The contact's identifier in the practice's CRM, which is what a marketing team uses to reconcile this record against the system staff work in day to day. |
created_at | TIMESTAMP | Created At | When the CRM record was opened, which is the moment of the first form submission or call rather than of any earlier visit. |
first_engagement_type | STRING | First Engagement Type | Which contact created the record: form or call. The two are the same funnel step, so this is what separates a channel's telephone demand from its web demand. |
is_qualified | BOOLEAN | Qualified on the Lead Record | Empty at this stage. The CRM carries the field from the start, but nobody has decided at this stage — the verdict is taken at a consultation and stored there. Counting empty as false will undercount the enquiries that were not accepted. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Person | person_id = person_id | N:1 | The human behind the enquiry, which is how a lead connects to anything that happened before it. |
Page
Every page of the practice's website that a visitor can land on, held once and reused by every view of it. Keeping pages as their own object rather than as a URL repeated on each view is what lets a clinic ask which kinds of page do the work: whether the enquiries that begin on a treatment page and the ones that begin on a pricing page fare the same once a clinician has looked at them. page_type is what lets that question be asked of a kind of page rather than of one URL — whether the visitor was reading about a treatment, checking prices, or browsing the blog.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
page_id | STRING | Page ID | PK. Unique identifier for this page. |
page_url | STRING | Page URL | Full address of the page, including its domain. |
page_path | STRING | Page Path | Path portion of the address, without domain or query string — the form that identifies a page across domains and tracking parameters. |
page_title | STRING | Page Title | Title shown in the browser tab and in search results. |
page_type | STRING | Page Type | What the page is for: home, treatment, pricing, contact, blog or booking_form. This is what makes a question about page effectiveness answerable at all. |
Page View
Every page a visitor actually opened, in the order they opened it. Where a page is described once and for all, a page view is one reading of it by one person at one moment — which is what turns a site map into a record of behaviour.
This mart reaches further back than anything else in the model: the very first page view is where a person's identity is minted — before a name, before an email, before anyone at the practice knows a prospective patient is there. It is also what makes "what did they read before they got in touch" answerable. sequence_in_session makes a journey readable as an order rather than a pile of URLs; is_entrance and is_exit mark its two ends: entrances say which pages bring people in, exits say where attention is lost, and those are two different lists.
time_on_page_seconds is measured from when the next page was opened, so it cannot be known for the last page of a visit. That value is empty rather than zero, and treating it as zero understates how long the site was read for.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
page_view_id | STRING | Page View ID | PK. Unique identifier for this one reading of a page by one person at one moment. |
session_id | STRING | Session ID | The visit this reading happened in, which is what puts it in order next to everything else seen during the same visit. FK to Session |
person_id | STRING | Person ID | The human who read the page. Carried here as well as on the visit, so a person's whole reading history can be assembled without walking through sessions. FK to Person |
page_id | STRING | Page ID | Which page was read. The view holds this, because one page is read many times. FK to Page |
viewed_at | TIMESTAMP | Viewed At | When the page was opened. On a person's earliest page view, this is also the moment their identity was minted. |
sequence_in_session | INTEGER | Position In Visit | Where this page sat in the visit, counting from 1 — what makes a journey readable as an order rather than a set of URLs. |
time_on_page_seconds | INTEGER | Time On Page (sec) | Seconds until the next page was opened. Empty for the last page of a visit, where there is no next page to measure against — not zero. |
is_entrance | BOOLEAN | Is Entrance | TRUE when this was the first page of the visit: the page that had to earn the visitor's attention. |
is_exit | BOOLEAN | Is Exit | TRUE when this was the last page of the visit — where the visitor stopped, whether satisfied or not. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Session | session_id = session_id | N:1 | The visit this page was seen during. |
| Person | person_id = person_id | N:1 | The human who viewed it. |
| Page | page_id = page_id | N:1 | Which page was viewed. |
Person
The human behind everything else in this model. The identity is minted at the very first page view — before anyone has given a name, an email or a phone number — and is never reissued, so a phone at lunchtime, a laptop weeks later and a call months after that all belong to one person. It keeps the source, medium and campaign of that first arrival frozen, so the ad that started a journey is still attached when the money arrives, whether that takes two weeks or twenty years.
Two levels of identity sit here, and keeping them apart is what makes attribution honest. visitor_id, carried on the session, is browser-level — the Google Analytics client_id — and changes with a new device, a cleared cookie or a different browser. person_id sits above it: several of those resolved to one human. Grouping by visitor_id counts devices; grouping by person_id counts people, and a practice's patient numbers only mean anything at the second level. This way of stitching a person together across devices and years is the one APAS® Cloud uses, described for this model by its co-founder.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
person_id | STRING | Person ID | PK. Unique identifier for the human, minted at their first page view and never reissued — one person across every device and browser they use. |
first_seen_at | TIMESTAMP | First Seen At | When the practice first saw this person: the moment of the first page view, which is also when the identity was minted. |
first_source | STRING | First Source | Where the person came from on that very first visit, such as google, facebook or direct. Frozen on the person, so it still describes a patient treated years later. |
first_medium | STRING | First Medium | How that first visit arrived, such as cpc, organic or referral — the split between paid and earned traffic at first touch. |
first_campaign | STRING | First Campaign | Campaign that brought the person in the first time. NULL when the first visit came from somewhere that carries no campaign, such as direct or organic search. |
first_landing_page_url | STRING | First Landing Page | Address of the page the person first landed on. Plain text rather than a link to a page record, because a site gets rewritten and that page may no longer exist. |
identified_at | TIMESTAMP | Identified At | When the person first gave contact details and stopped being anonymous. NULL for someone who has only ever browsed. |
Revenue
Money the practice has actually received. Issuing a bill creates a claim; only a payment creates revenue, and the two are kept apart because they are not the same event and do not happen at the same time. A single invoice can be settled in more than one payment, or in none, so this mart is joined to the invoice rather than folded into it.
A treatment is paid for in two pieces. The patient's copay is collected before the treatment starts and is revenue straight away, though a small share of the total. The rest comes from the insurer, after the claim is submitted and with a lag; days_to_payment records that wait, and read with payment_source it says who the practice is waiting on. That lag is why a total taken over the month a treatment happened is not the same as one taken over the month the money landed.
Revenue reaches advertising through the patient, whose person_id goes back to the first visit and the campaign frozen on it. One patient's payments can spread over years, so a campaign total sums a whole history rather than a single purchase.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
revenue_id | STRING | Revenue ID | PK. Unique identifier for one payment received. It is an event, not a balance: one bill can be settled by several rows here. |
customer_id | STRING | Customer ID | The patient the money came in for. Through them the payment reaches the person record, which is how revenue is credited back to the visit and the advertising that started it. FK to Customer (Patient) |
invoice_id | STRING | Invoice ID | The bill this payment settles, in whole or in part. Comparing the two is how a practice sees what is still outstanding. FK to Invoice |
received_at | TIMESTAMP | Received At | When the money arrived. This is the date revenue belongs to, and for an insurer's part it can fall in a later period than the treatment it pays for. |
amount | NUMERIC | Amount Received | How much actually arrived in this payment. It can be less than the bill asked for, and a bill can be settled by several payments. |
currency | STRING | Payment Currency | Currency the money arrived in, such as USD. Needed before payments can be summed together. |
payment_source | STRING | Payment Source | Where the money came from: patient_copay, collected before the treatment starts and a small share of the total, or insurance_reimbursement, paid after the claim is processed. One arrives before the treatment and the other after the claim is processed, so cash-flow questions need this split. |
days_to_payment | INTEGER | Days to Payment | How long this money took to arrive, counted from the bill. It is the reimbursement lag made measurable: read by payment_source it separates the copay collected before treatment from the insurer's part, and tracked over time it shows whether insurers are paying more slowly. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Customer (Patient) | customer_id = customer_id | N:1 | Who the money came in for. |
| Invoice | invoice_id = invoice_id | N:1 | The bill this payment settles, in whole or in part. |
Session
One visit to the practice's website, from the moment someone arrives to the moment they go quiet; everything seen or clicked in between belongs to it. A session carries the channel that brought this particular visit, the device it happened on and where in the world it came from, so it answers "what was this trip for", while the person record answers "who was this and what first brought them here".
visitor_id is browser-level — the Google Analytics client_id — lost when someone switches device, clears cookies or changes browser; person_id sits above it. Counting distinct visitors counts browsers, counting distinct people counts prospective patients, and only the second means anything for a practice's own numbers.
The UTM fields describe this visit and are deliberately not the first-touch source frozen on the person: someone who arrived through an ad and returned through a branded search has one record of each, which separates the channel that creates demand from the channel that merely collects it. campaign, term and content are empty for visits that carry no campaign at all, the normal state for direct and organic arrivals.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
session_id | STRING | Session ID | PK. Unique identifier for this visit. Every page view and every ad click that landed in the visit carries it. |
person_id | STRING | Person ID | The human this visit belongs to, which is how visits made months apart and on different devices add up to one journey. FK to Person |
visitor_id | STRING | Visitor ID | Browser-level identifier the visit was recorded under — one browser on one device. Several of these belong to one person, so counting them counts devices, not patients. |
started_at | TIMESTAMP | Started At | When the visit began, which is the moment of its first page view. |
ended_at | TIMESTAMP | Ended At | When the visit was closed off after the visitor went quiet. The gap from started_at is the time spent on site. |
source | STRING | Source | Where this particular visit came from, such as google, facebook or direct — the source of this trip, not of the person's first ever arrival. |
medium | STRING | Medium | How the visit arrived, such as cpc, organic or referral — the line between traffic that was paid for and traffic that was not. |
campaign | STRING | Campaign | Campaign behind this visit. Empty for arrivals that carry no campaign, such as direct traffic or unpaid search. |
term | STRING | Search Term | Keyword the visit was bought or found under, where the channel supplies one. |
content | STRING | Creative | Which creative or link variant brought this visit, matching the creative identifier carried on the ad click. |
landing_page_url | STRING | Landing Page | Address of the first page seen in this visit — the page that had to do the persuading. |
device_type | STRING | Device Type | What the visit happened on: mobile, desktop or tablet. One person's visits can be spread across several devices, which is why this sits on the visit rather than on the person. |
browser | STRING | Browser | Browser the visit was made in. Useful for spotting tracking that has broken in one browser and not the others. |
country | STRING | Country | Country the visit came from, which is what separates local demand from traffic a single-site practice can never treat. |
is_first_session | BOOLEAN | Is First Session | TRUE for the one visit during which this person's identity was minted, and FALSE for every return. Splits new prospects from returning ones without recomputing the journey. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Person | person_id = person_id | N:1 | The human this visit belongs to, across every device they use. |
Treatment
The step between a clinician saying yes and the practice being paid. Once a consultation qualifies someone a date is agreed and the treatment is booked, the patient's copay is collected before it starts, and the claim goes to the insurer after it. Every bill and every payment in this model is raised against one of these rows: before this record the model describes demand, after it money.
Being qualified and being treated are two different things. Someone can be cleared and still decide not to go ahead, a drop-off that stays invisible unless bookings are counted separately from verdicts. booked_at and scheduled_date are deliberately separate too: one is when the appointment was agreed, the other the day it was set for, and the distance between them is the practice's waiting time.
A treatment points back at the consultation that cleared it, forward at the patient receiving it and at the clinician delivering it — the same staff list the screenings point at — so a clinician's verdict and the money that followed sit on one chain, and advertising can be read against treatments delivered, not only enquiries taken.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
treatment_id | STRING | Treatment ID | PK. Unique identifier for one booked treatment, which is what every invoice and every payment in this model is ultimately raised against. |
consultation_id | STRING | Consultation ID | The assessment that cleared this treatment to go ahead. It is the step that carries a clinician's verdict forward into the money, and the enquiry and its advertising can be reached through it. FK to Consultation |
customer_id | STRING | Customer ID | The patient being treated, as the billing side of the practice knows them. FK to Customer (Patient) |
employee_id | STRING | Employee ID | The clinician delivering the treatment, drawn from the same staff list that holds the screening. It allows one person's assessments and their delivered work to be compared. FK to Employee |
booked_at | TIMESTAMP | Booked At | When the appointment was agreed, not when it happens. Measured against the consultation, it is how long the practice took to convert a yes into a booking. |
scheduled_date | DATE | Scheduled Date | The day the treatment is set for. The distance from booked_at is the waiting time a patient is asked to accept. |
completed_date | DATE | Completed Date | The day the treatment actually happened. Empty while it is still in the diary or was cancelled, which is what separates delivered work from booked work. |
treatment_name | STRING | Treatment Name | What is being done. It is what makes a channel's demand readable as a case mix rather than as a single count. |
status | STRING | Treatment Status | Where the booking stands: booked while it is ahead, completed once delivered, cancelled when it will not happen. Cancellations are qualified patients who did not get treated, so they belong in a funnel count rather than being dropped. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Consultation | consultation_id = consultation_id | N:1 | The assessment that cleared this treatment to go ahead. |
| Customer (Patient) | customer_id = customer_id | N:1 | Who is being treated. |
| Employee | employee_id = employee_id | N:1 | The clinician delivering it. |
- 1
Install the Import Model plugin
One plugin, installed once, in your own OWOX workspace.
- 2
Import this model
Point it at this bundle and it creates every data mart above, joins and all.
- 3
Plug in your data and destinations
Connect your own sources and send the results where your team already works.
4. Optional — customize as you wish. Rename a column, drop a mart, add your own: once it is imported it is yours, and nothing here syncs back.