simplepractice-mcp 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +26 -0
- package/.claude-plugin/plugin.json +13 -0
- package/.mcp.json +11 -0
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/dist/auth.js +79 -0
- package/dist/bundle.js +32350 -0
- package/dist/client.js +177 -0
- package/dist/config.js +58 -0
- package/dist/index.js +26 -0
- package/dist/jsonapi.js +91 -0
- package/dist/tools/account.js +48 -0
- package/dist/tools/appointments.js +54 -0
- package/dist/tools/auth.js +67 -0
- package/dist/tools/billing.js +88 -0
- package/dist/tools/documents.js +94 -0
- package/dist/version.js +5 -0
- package/package.json +48 -0
- package/server.json +34 -0
- package/skills/simplepractice/SKILL.md +74 -0
- package/skills/simplepractice-fpx/SKILL.md +260 -0
- package/skills/simplepractice-fpx/references/requests.md +398 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
# SimplePractice Client Portal — request reference
|
|
2
|
+
|
|
3
|
+
Base: `https://<practice>.clientsecure.me/client-portal-api`
|
|
4
|
+
|
|
5
|
+
Every shape below was taken from the portal app's own published sourcemaps
|
|
6
|
+
(`widget-cdn.simplepractice.com/assets/*.map`, which ship full
|
|
7
|
+
`sourcesContent`) and then confirmed against a live signed-in portal. Nothing
|
|
8
|
+
here is guessed. Where a field could not be exercised on the account used for
|
|
9
|
+
verification, it says so.
|
|
10
|
+
|
|
11
|
+
Assumes the `sp()` helper and `$SP_API` from `SKILL.md`.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 0. Two naming systems — the trap
|
|
16
|
+
|
|
17
|
+
URLs are **dashed and plural**. JSON:API `type` values are **camelCase and
|
|
18
|
+
plural**. They are not the same string, and one endpoint uses both:
|
|
19
|
+
|
|
20
|
+
| URL path | `.data[].type` |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `/sign-in-tokens` | `signInTokens` |
|
|
23
|
+
| `/document-requests` | `documentRequestQuestionnaires`, `documentRequestConsentDocuments`, … |
|
|
24
|
+
| `/billing-items` | `invoices`, `statements`, `superbills`, `receipts`, `payments` |
|
|
25
|
+
| `/client-billing-overviews` | `clientBillingOverviews` |
|
|
26
|
+
| `/environment` (singular!) | `environments` |
|
|
27
|
+
|
|
28
|
+
So never build a `jq` filter by pluralising the path. Match on the `type`
|
|
29
|
+
string the response actually carries, or select positionally.
|
|
30
|
+
|
|
31
|
+
`/environment` is the one singular path in the API.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 1. Auth
|
|
36
|
+
|
|
37
|
+
### 1.1 Request a magic link — `POST /sign-in-tokens`
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
sp -X POST "$SP_API/sign-in-tokens" \
|
|
41
|
+
-H 'Content-Type: application/vnd.api+json' \
|
|
42
|
+
--data '{"data":{"type":"sign-in-tokens","attributes":{"email":"you@example.com","expiresIn":"15 minutes"}}}'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`202 Accepted`:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{"data":{"id":"…","type":"signInTokens","attributes":{"email":"you@example.com","expiresIn":"24 hours"}}}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- `expiresIn` in the **response** is the real lifetime (24 hours) whatever you
|
|
52
|
+
request. The portal app deliberately shows the same "24 hours" wording for an
|
|
53
|
+
address with no account, so that the response cannot be used to test whether
|
|
54
|
+
an email is registered. A `202` is therefore not proof the address exists.
|
|
55
|
+
- Optional `redirect` attribute: a portal-relative path to land on after
|
|
56
|
+
verifying (the app uses it for `payment-link/<id>`).
|
|
57
|
+
- **Errors.** `429` with title `Email request limit reached` or
|
|
58
|
+
`IP request limit reached`; `422` for a malformed address. Do not retry
|
|
59
|
+
either — this is the only auth path the portal has.
|
|
60
|
+
|
|
61
|
+
### 1.2 Exchange the token — `POST /sessions/token`
|
|
62
|
+
|
|
63
|
+
The emailed link is `https://<practice>.clientsecure.me/sign-in/token/verify#<TOKEN>`.
|
|
64
|
+
The token is the **fragment**. A browser never sends a fragment to the server;
|
|
65
|
+
the Ember app reads `location.hash` and posts it. Fetching the link with `curl`
|
|
66
|
+
accomplishes nothing — copy the part after `#`.
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
sp -X POST "$SP_API/sessions/token" \
|
|
70
|
+
-H 'Content-Type: application/vnd.api+json' \
|
|
71
|
+
--data '{"data":{"type":"sessions","attributes":{"type":"token","token":"'"$TOKEN"'"}}}'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Success sets the `simplepractice-session` cookie (Rails/Devise) and returns
|
|
75
|
+
`.data.meta.status`:
|
|
76
|
+
|
|
77
|
+
| `meta.status` | meaning |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `verified` | signed in; the cookie jar is now good |
|
|
80
|
+
| `expired` | older than 24h — request a new link |
|
|
81
|
+
| `merged` | the account was merged into another; sign in from the new portal |
|
|
82
|
+
|
|
83
|
+
Tokens are single-use; replaying one gives `401`/`422`.
|
|
84
|
+
|
|
85
|
+
> Not exercised live during this build: the exchange call itself. §1.1 was
|
|
86
|
+
> confirmed against a real account (`202`), and the shape above comes from
|
|
87
|
+
> `routes/sign-in/token/verify.js` + `services/sign-in.js`, but reading the
|
|
88
|
+
> emailed token was out of scope for the agent that wrote this. Treat
|
|
89
|
+
> `meta.status` handling as source-derived until you've run it once.
|
|
90
|
+
|
|
91
|
+
### 1.3 PIN variant — `POST /sessions/pin`
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
sp -X POST "$SP_API/sessions/pin" \
|
|
95
|
+
-H 'Content-Type: application/vnd.api+json' \
|
|
96
|
+
--data '{"data":{"type":"sessions","attributes":{"type":"pin","email":"you@example.com","pin":"123456"}}}'
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The PIN is exactly 6 digits (client-side regex `^\d{6}$`) and is likewise
|
|
100
|
+
single-use — a wrong or reused code comes back as a validation error on `pin`,
|
|
101
|
+
and `429` is the rate limit.
|
|
102
|
+
|
|
103
|
+
### 1.4 Session expiry
|
|
104
|
+
|
|
105
|
+
Any endpoint answers `401 {"errors":[{"title":"You have no access to this client","status":"401"}]}`
|
|
106
|
+
once the cookie lapses. There is no refresh token: re-run §1.1.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 2. Identity — `GET /environment`
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
sp "$SP_API/environment?include=currentPractice,currentClient,currentClientOptions"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`.data` is the singleton `environments` record; the interesting part is
|
|
117
|
+
`.data.relationships` + `.included[]`:
|
|
118
|
+
|
|
119
|
+
| relationship | shape |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `currentPractice` | one `practices` |
|
|
122
|
+
| `currentClient` | one `clients` — whose data every other endpoint returns |
|
|
123
|
+
| `currentClientOptions` | **array** of `clients` this login may switch between |
|
|
124
|
+
| `currentClientAccess` | one `clientAccesses` — the login itself |
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
sp "$SP_API/environment?include=currentPractice,currentClientOptions" | jq '{
|
|
128
|
+
practice: (.included[] | select(.type=="practices") | .attributes.fullName),
|
|
129
|
+
timeZone: (.included[] | select(.type=="practices") | .attributes.timeZone),
|
|
130
|
+
clients: [.included[] | select(.type=="clients")
|
|
131
|
+
| {id, name: ((.attributes.preferredName // .attributes.firstName) + " " + .attributes.lastName)}]
|
|
132
|
+
}'
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Useful `practices` attributes (67 in all): `fullName`, `timeZone`, `currency`,
|
|
136
|
+
`practiceUrl`, `phoneNumber`, `isGroupPractice`, `telehealthEnabled`,
|
|
137
|
+
`selfSchedulingEnabled`, `isClientAllowedToCancelAppt`,
|
|
138
|
+
`isClientAllowedToConfirmAppt`, `clientCancellableHrs`,
|
|
139
|
+
`announcementsAvailable`, `featureSecureMessagingEmber`.
|
|
140
|
+
|
|
141
|
+
`isClientAllowedToCancelAppt` and `clientCancellableHrs` are the practice's
|
|
142
|
+
actual cancellation policy — worth reading before assuming an appointment can
|
|
143
|
+
be cancelled.
|
|
144
|
+
|
|
145
|
+
`clients` attributes include `firstName`, `lastName`, `preferredName`,
|
|
146
|
+
`nickname`, `birthDate`, `hashedId`, `status`, `billingType`,
|
|
147
|
+
`hasIncompleteDocument`, `hasNewAnnouncements`, `hasInvoicedAppointments`,
|
|
148
|
+
`permissions`, and `relationshipToCurrentClientAccess`.
|
|
149
|
+
|
|
150
|
+
**`clients[].email` is `null` on a login that acts for someone else** (a parent
|
|
151
|
+
portal, say). The sign-in address belongs to the *access*, not the client.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 3. Appointments — `GET /appointments`
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
# upcoming / confirmed
|
|
159
|
+
sp "$SP_API/appointments?include=clinician,office,client&filter[hasPendingConfirmation]=false&page[size]=50&page[number]=1"
|
|
160
|
+
# requested, awaiting the practice's confirmation
|
|
161
|
+
sp "$SP_API/appointments?include=clinician,office,client&filter[hasPendingConfirmation]=true&page[size]=50&page[number]=1"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`.data[].type` is `appointments`; `.included[]` carries `clinicians`,
|
|
165
|
+
`offices`, `clients`.
|
|
166
|
+
|
|
167
|
+
Attributes (21 live; from `models/unauthenticated-appointment.js` +
|
|
168
|
+
`models/appointment.js`):
|
|
169
|
+
|
|
170
|
+
| field | notes |
|
|
171
|
+
|---|---|
|
|
172
|
+
| `startTime`, `endTime` | ISO-8601 with offset |
|
|
173
|
+
| `serviceDescription` | e.g. the CPT service name |
|
|
174
|
+
| `confirmationStatus`, `clientConfirmationStatus` | practice-side vs client-side |
|
|
175
|
+
| `isCancellable` | boolean — respects the practice's own policy |
|
|
176
|
+
| `cancelReason`, `visitReason`, `visitTherapyReasons` | |
|
|
177
|
+
| `videoRoomUrl` | telehealth link, when the appointment is video |
|
|
178
|
+
| `icalUrl`, `gcalendarUrl` | ready-made calendar links |
|
|
179
|
+
| `fee`, `uninvoicedFee`, `billableDescription`, `cptCodes`, `units` | |
|
|
180
|
+
| `channel`, `schedulingSource`, `source` | how it was booked |
|
|
181
|
+
| `files` | attachments |
|
|
182
|
+
|
|
183
|
+
Relationships: `clinician`, `office`, `client`, `card`, `superbill`,
|
|
184
|
+
`invoiceItems`, `appointmentClient`.
|
|
185
|
+
|
|
186
|
+
`offices` carry `name`, `street`, `city`, `state`, `zip`, `phone`, `isVideo`,
|
|
187
|
+
`geolocation` — `isVideo: true` is a telehealth "room", not an address.
|
|
188
|
+
|
|
189
|
+
Joined one-liner:
|
|
190
|
+
|
|
191
|
+
```sh
|
|
192
|
+
sp "$SP_API/appointments?include=clinician,office&filter[hasPendingConfirmation]=false&page[size]=50&page[number]=1" \
|
|
193
|
+
| jq -r '
|
|
194
|
+
(.included // []) as $inc
|
|
195
|
+
| .data[]
|
|
196
|
+
| . as $a
|
|
197
|
+
| ($inc[]? | select(.type=="clinicians" and .id==$a.relationships.clinician.data.id)) as $c
|
|
198
|
+
| ($inc[]? | select(.type=="offices" and .id==$a.relationships.office.data.id)) as $o
|
|
199
|
+
| [$a.attributes.startTime,
|
|
200
|
+
($a.attributes.serviceDescription // "—"),
|
|
201
|
+
"\($c.attributes.firstName) \($c.attributes.lastName)",
|
|
202
|
+
(if $o.attributes.isVideo then "telehealth" else ($o.attributes.name // "—") end)
|
|
203
|
+
] | @tsv'
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Pagination: by number.** `page[number]` / `page[size]`, max size 50. A short
|
|
207
|
+
page is the last page.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## 4. Billing — `GET /billing-items`
|
|
212
|
+
|
|
213
|
+
One polymorphic collection, switched by `filter[thisType]`:
|
|
214
|
+
|
|
215
|
+
| `filter[thisType]` | `.data[].type` | key attributes |
|
|
216
|
+
|---|---|---|
|
|
217
|
+
| `invoice` | `invoices` | `displayName`, `displayStatus`, `invoiceDate`, `totalAmount`, `remainingAmount`, `isNewForClient` |
|
|
218
|
+
| `statement` | `statements` | `displayName`, `createdAt`, `isNewForClient` |
|
|
219
|
+
| `superbill` | `superbills` | `displayName`, `createdAt`, `totalAmount`, `isNewForClient` |
|
|
220
|
+
| `receipt` | `receipts` | `displayName`, `createdAt`, `isNewForClient` |
|
|
221
|
+
| `billable-item,payment` (+ `filter[thisTypeCondition]=unallocated`) | mixed | account history |
|
|
222
|
+
|
|
223
|
+
```sh
|
|
224
|
+
sp "$SP_API/billing-items?filter[thisType]=invoice&page[size]=50" \
|
|
225
|
+
| jq '{balance: .meta.endBalance,
|
|
226
|
+
rows: [.data[] | {type, id, name: .attributes.displayName,
|
|
227
|
+
status: .attributes.displayStatus,
|
|
228
|
+
total: .attributes.totalAmount,
|
|
229
|
+
due: .attributes.remainingAmount}]}'
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Every billing query returns `.meta.endBalance`.
|
|
233
|
+
|
|
234
|
+
Optional `filter[timeRange]` narrows by date; the portal sends it as a
|
|
235
|
+
`{start,end}` object, which `curl` writes as
|
|
236
|
+
`filter[timeRange][start]=…&filter[timeRange][end]=…`. Omit it for everything.
|
|
237
|
+
|
|
238
|
+
**Pagination: by cursor, backwards.** `page[size]=50`, then
|
|
239
|
+
`page[before]=<the last row's cursorId>`. The cursor is the row's `cursorId`
|
|
240
|
+
attribute, **not** its `id`. A short page is the last page.
|
|
241
|
+
|
|
242
|
+
```sh
|
|
243
|
+
sp "$SP_API/billing-items?filter[thisType]=invoice&page[size]=50" | jq -r '.data[-1].attributes.cursorId'
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
An empty `.data[]` here is a normal, correct answer — many practices invoice
|
|
247
|
+
entirely outside the portal. All five filters were confirmed to return `200`
|
|
248
|
+
with `meta.endBalance` on the account used for verification, which had no
|
|
249
|
+
portal billing rows.
|
|
250
|
+
|
|
251
|
+
### 4.1 Balance summary and saved cards live ON the client record
|
|
252
|
+
|
|
253
|
+
There is **no** `/client-billing-overviews` collection and **no** `/cards`
|
|
254
|
+
collection. Both are `include`-able relationships of `/clients/<id>`:
|
|
255
|
+
|
|
256
|
+
```sh
|
|
257
|
+
CLIENT_ID=$(sp "$SP_API/environment?include=currentClient" \
|
|
258
|
+
| jq -r '.data.relationships.currentClient.data.id')
|
|
259
|
+
sp "$SP_API/clients/$CLIENT_ID?include=clientBillingOverview,cards"
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
- `clientBillingOverviews` — `balanceDue`, `unallocatedPaymentAmount`, and the
|
|
263
|
+
counts `invoicesCount` / `statementsCount` / `superbillsCount` /
|
|
264
|
+
`receiptsCount` / `insuranceInfoCount`. Cheaper than paging the collections
|
|
265
|
+
just to see whether anything is there.
|
|
266
|
+
- `cards` — `brand`, `last4`, `expiry` (e.g. `"07 / 30"`), `expMonth`,
|
|
267
|
+
`expYear`, `isDefault` (a **string** `"true"`/`"false"`), plus the Stripe
|
|
268
|
+
identifiers `paymentMethodId` / `customStripeCardId` /
|
|
269
|
+
`customStripeCustomerId`. No full card number.
|
|
270
|
+
|
|
271
|
+
> Guessing `/cards` and `/client-billing-overviews` is the natural first move,
|
|
272
|
+
> and both return **HTTP 200** — with `text/html` and the app shell, because
|
|
273
|
+
> the SPA catch-all swallows every undefined path. They read as working,
|
|
274
|
+
> empty endpoints. This cost a full debugging round during this build; check
|
|
275
|
+
> `content-type`, never status, when an endpoint returns suspiciously nothing.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## 5. Documents — `GET /document-requests`
|
|
280
|
+
|
|
281
|
+
Paperwork the practice has sent you to read, complete or sign.
|
|
282
|
+
|
|
283
|
+
```sh
|
|
284
|
+
sp "$SP_API/document-requests?page[size]=50" \
|
|
285
|
+
| jq -r '.data[] | [.attributes.status, .type, .attributes.documentTitle] | @tsv'
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`.data[].type` is the *subtype*, and the attribute set varies with it:
|
|
289
|
+
|
|
290
|
+
| `type` | what it is |
|
|
291
|
+
|---|---|
|
|
292
|
+
| `documentRequestConsentDocuments` | a consent form to sign |
|
|
293
|
+
| `documentRequestQuestionnaires` | a questionnaire — `templateQuestions`, `userAnswers` |
|
|
294
|
+
| `documentRequestContactInfos` | demographics/contact form |
|
|
295
|
+
| `documentRequestInsuranceInfos` | insurance details |
|
|
296
|
+
| `documentRequestCreditCardInfos` | card on file — `cardAttributes` |
|
|
297
|
+
| `documentRequestStoredDocuments` | a file shared with you |
|
|
298
|
+
| `documentRequestNotes` | a note |
|
|
299
|
+
| `documentRequestGoodFaithEstimates` | a Good Faith Estimate |
|
|
300
|
+
| `documentRequestPostSessionSummaries` | post-session summary |
|
|
301
|
+
|
|
302
|
+
`status` ∈ `sent` · `viewed` · `reviewing` · `completed` · `locked`
|
|
303
|
+
(`completed`, `sent` and `viewed` seen live). Anything not `completed`/`locked`
|
|
304
|
+
is outstanding:
|
|
305
|
+
|
|
306
|
+
```sh
|
|
307
|
+
sp "$SP_API/document-requests?page[size]=50" \
|
|
308
|
+
| jq -r '[.data[] | select(.attributes.status | IN("completed","locked") | not)
|
|
309
|
+
| .attributes.documentTitle] | "outstanding: \(length)\n" + join("\n")'
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Common attributes: `documentTitle`, `status`, `createdAt`, `updatedAt`,
|
|
313
|
+
`hasDocumentPdf`.
|
|
314
|
+
|
|
315
|
+
> **`hasDocumentPdf` is a JSON string**, `"true"` or `"false"` — not a boolean,
|
|
316
|
+
> despite `models/document-request.js` declaring `@attr('boolean')` (Ember casts
|
|
317
|
+
> it client-side; the wire value is a string). Both values were seen live. So
|
|
318
|
+
> `select(.attributes.hasDocumentPdf)` matches every row, including the ones
|
|
319
|
+
> with no PDF. Always compare to the string:
|
|
320
|
+
>
|
|
321
|
+
> ```sh
|
|
322
|
+
> jq -r '.data[] | select(.attributes.hasDocumentPdf == "true") | .attributes.documentTitle'
|
|
323
|
+
> ```
|
|
324
|
+
>
|
|
325
|
+
> It is not the only one: a saved card's `isDefault` arrives as `"true"` /
|
|
326
|
+
> `"false"` too. The declared type in the model is not evidence of the wire
|
|
327
|
+
> type — check any boolean you come to depend on, with a real response. Subtype-specific: `documentType`, `documentExt`,
|
|
328
|
+
`documentMimeType`, `documentBody`, `templateQuestions`, `userAnswers`,
|
|
329
|
+
`cardAttributes`, `mixpanelType`.
|
|
330
|
+
|
|
331
|
+
Collection `.meta` carries `hasDocumentsIntro` and `welcomeText`.
|
|
332
|
+
|
|
333
|
+
Single request: `GET /document-requests/<id>`.
|
|
334
|
+
|
|
335
|
+
`hasDocumentPdf: true` means a rendered PDF exists. The portal fetches it
|
|
336
|
+
through the same authenticated origin; treat the URL as session-scoped.
|
|
337
|
+
|
|
338
|
+
`GET /documents` is the separate "files shared with you" list —
|
|
339
|
+
`documentName`, `documentExt`, `thisType`, `createdAt`.
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## 6. Announcements — `GET /announcements`
|
|
344
|
+
|
|
345
|
+
```sh
|
|
346
|
+
sp "$SP_API/announcements?page[size]=50" \
|
|
347
|
+
| jq -r '.data[] | [(.attributes.readAt // "UNREAD"), .attributes.title] | @tsv'
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Attributes: `title`, `message`, `fromLabel`, `createdAt`, `readAt`,
|
|
351
|
+
`isDeleted`. `clients[].hasNewAnnouncements` (§2) is the cheap "is there
|
|
352
|
+
anything new" flag.
|
|
353
|
+
|
|
354
|
+
There is a `POST /announcements/read-announcements` that marks them all read —
|
|
355
|
+
a write, so out of scope here; it is listed only so you recognise it.
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## 7. Secure messaging
|
|
360
|
+
|
|
361
|
+
Messaging is **not** on this API. It lives at
|
|
362
|
+
`https://messaging-api.simplepractice.com` (`messagingApiUrl` in the portal's
|
|
363
|
+
config) with models `messagingConversation` / `messagingMessage` /
|
|
364
|
+
`messagingContact` / `messagingProfile` / `messagingUser`, and is gated by the
|
|
365
|
+
practice's `featureSecureMessagingEmber` flag.
|
|
366
|
+
|
|
367
|
+
Its request shapes were **not** captured for this skill. If you need messages,
|
|
368
|
+
read them in the portal, or capture the host's calls first — don't guess them.
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## 8. Errors
|
|
373
|
+
|
|
374
|
+
| status | meaning |
|
|
375
|
+
|---|---|
|
|
376
|
+
| `400` `Application build version is missing` | you dropped `Application-Build-Version` |
|
|
377
|
+
| `401` `You have no access to this client` | cookie stale/absent → re-auth (§1) |
|
|
378
|
+
| `422` | validation — the body names the offending field |
|
|
379
|
+
| `429` | rate limit; on auth calls the title says email- or IP-scoped. Do not retry |
|
|
380
|
+
|
|
381
|
+
Errors are JSON:API: `.errors[] | {title, code, status}`.
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## Appendix — where these shapes came from
|
|
386
|
+
|
|
387
|
+
The portal serves public sourcemaps with full original sources:
|
|
388
|
+
|
|
389
|
+
```sh
|
|
390
|
+
curl -s https://widget-cdn.simplepractice.com/assets/<chunk>.js.map | gunzip > map.json
|
|
391
|
+
node -e 'const m=require("./map.json");m.sources.forEach((s,i)=>{/* write m.sourcesContent[i] */})'
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
The chunk filenames are hashed per deploy — read them out of the portal HTML's
|
|
395
|
+
`<script src>` tags. `adapters/application.js` defines the namespace and the
|
|
396
|
+
required headers; `models/*.js` define every attribute; `routes/site/**` show
|
|
397
|
+
which filters each screen sends. When SimplePractice ships a new build, that is
|
|
398
|
+
the authoritative place to re-check a shape.
|