PIMS Partner API
Bidirectional integration between your practice management system and Coggo, the AI veterinary scribe.
- PIMS → Coggo: send signed webhook events when appointments, clients or patients change. Coggo upserts the records and a
scheduledvisit appears for the vet. - Coggo → PIMS: Coggo calls endpoints hosted by your PIMS for patient search, upcoming-appointment reconciliation, and to push signed SOAP notes back in
json,textandhtml.
Conventions
All ids you send are your external ids — Coggo maintains the mapping. Events are idempotent on id; retries are always safe. The PIMS is the source of truth for client/patient demographics; Coggo owns clinical documents and your PIMS never mutates those.
Base URL
https://api.coggo.ai/v1/partner
Integration flow
1. Clinic creates the integration in
Coggo -> Settings -> Integrations
(Pro plan) and shares with you:
- API key cgk_live_...
- signing secret cgs_...
2. GET /ping verify credentials
3. POST /webhooks appointments flow in
4. Implement 3 endpoints on your side
-> search, refresh, SOAP push-back Authentication
API key. Every call to Coggo carries Authorization: Bearer cgk_live_…. Issued once in Coggo settings; scoped and revocable.
Signatures (optional). The Bearer key alone authorizes your inbound webhooks over HTTPS. For extra protection you may also send the headers below — when present, Coggo verifies them. Coggo always signs its outbound pushes to you.
| X-Coggo-Idstringoptional | Unique request/event id. Also the idempotency key (recommended even without a signature). |
| X-Coggo-Timestampintegeroptional | Unix seconds. If sent, requests older or newer than 300s are rejected. |
| X-Coggo-Signaturestringoptional | Base64 HMAC-SHA256 of id.timestamp.body using the shared secret. Verified only when present. |
Simplest webhook (Bearer key only)
curl -X POST https://api.coggo.ai/v1/partner/webhooks \
-H "Authorization: Bearer cgk_live_..." \
-H "Content-Type: application/json" \
-d '{"id":"evt_1","type":"appointment.created","occurredAt":"...","data":{...}}' Optional: add an HMAC signature (bash)
SECRET="cgs_..."
ID="evt_test1"
TS=$(date +%s)
BODY='{"id":"evt_test1", ... }'
SIG=$(printf '%s' "$ID.$TS.$BODY" \
| openssl dgst -sha256 -hmac "$SECRET" -binary \
| base64) Verify credentials
curl https://api.coggo.ai/v1/partner/ping \ -H "Authorization: Bearer cgk_live_..."
Verify credentials
Confirms the API key is valid and returns the integration's state. Use it as the first step of any setup and as a health check.
Response fields
| okboolean | Always true on success. |
| clinicIdstring | The Coggo clinic this key is scoped to. |
| providerstring | Provider name set at creation, e.g. woovet. |
| scopesstring[] | Granted scopes. |
| statusstring | active or paused. |
Response · 200
{
"ok": true,
"clinicId": "1ee77e8d-...",
"provider": "woovet",
"scopes": ["read:patients",
"write:appointments",
"write:clients",
"write:patients",
"read:notes"],
"status": "active"
} Send an event
Requires the Bearer key; the signature headers are optional (sent them to add HMAC verification). Duplicate ids are acknowledged with "duplicate": true and skipped, so retry freely.
Event types
Body fields
| idstringrequired | Your unique event id (idempotency key). |
| typestringrequired | One of the event types above. |
| occurredAtdate-timerequired | When it happened in your system. |
| data.appointmentobject | externalId, startsAt (local clinic time) + timezone (IANA), durationMinutes, reason, externalStaffId, status. Required for appointment events. |
| data.patientobjectrequired for appt. | Required on appointment.created/appointment.updated — the patient is upserted first, then the appointment links to it. Missing it returns 400 patient_required. Cancellations may omit it. |
| data.clientobject | externalId + name, email, phone, address fields. Embed on appointment events so no callback is needed. |
| patient fieldsobject | externalId, name (required); speciesKey (preferred, mapped — see Species mapping) or legacy species; breed, color (auto-added per clinic if new); sex, reproductiveStatus; dateOfBirth or age {value, unit}; weight {value, unit: kg|lb} (or legacy weightKg); externalClientId. |
| data.mergedIntoExternalIdstring | For patient.merged — the surviving patient's external id. |
What Coggo does
Upserts client → patient → creates a scheduled visit. Before creating, Coggo tries to link to existing records (unique email/phone for clients, unique name for patients) to avoid duplicates.
Request · appointment.created
{
"id": "evt_8f2c41",
"type": "appointment.created",
"occurredAt": "2026-06-12T09:30:00Z",
"data": {
"appointment": {
"externalId": "apt_1029",
"startsAt": "2026-06-12T14:00:00",
"timezone": "America/New_York",
"reason": "Lameness re-check",
"externalStaffId": "stf_44"
},
"client": {
"externalId": "cli_553",
"firstName": "Peggy",
"lastName": "Bartl",
"email": "[email protected]",
"phone": "+15134041567"
},
"patient": {
"externalId": "pat_771",
"name": "Sky",
"speciesKey": "1",
"breed": "Golden Retriever",
"color": "Golden",
"sex": "female",
"reproductiveStatus": "spayed",
"dateOfBirth": "2023-06-08",
"weight": { "value": 11, "unit": "kg" }
}
}
} Response · 200
{ "received": true }
// replayed event:
{ "received": true, "duplicate": true } Species mapping you implement
Return your PIMS species catalog so the clinic can map each PIMS species to a Coggo species once, during setup. After mapping, your patient payloads send speciesKey (the key below) and Coggo resolves the Coggo species automatically — no name-matching guesswork.
A new integration starts in pending mapping and only activates once every species is mapped. Coggo calls this at setup and when the clinic clicks "Refresh species". If a PIMS species has no Coggo equivalent, the clinic can add it as a new species for their account.
Response · 200
| species[].keystringrequired | Your PIMS species id / foreign key. Echo this back as patient.speciesKey. |
| species[].labelstring | Human-readable name shown in the clinic's mapping screen. |
Response · 200
{
"species": [
{ "key": "1", "label": "Canine" },
{ "key": "2", "label": "Feline" },
{ "key": "7", "label": "Equine" }
]
} Then on patients
"patient": {
"externalId": "P-1042",
"name": "Kiva",
"speciesKey": "1",
"breed": "Labrador",
"color": "Black",
"weight": { "value": 13.2, "unit": "kg" },
"age": { "value": 6, "unit": "years" }
} Patient search you implement
Called when a Coggo user searches a patient that isn't in Coggo and clicks "Search in PIMS". Respond within 2 seconds with up to 20 matches. Coggo shows the results and imports the selected patient (creating the client, patient, and id links).
Each result is shown in the search list and imported with one click, so include as much as you have. Coggo displays and imports the patient's name, species, breed and sex, plus the owner's firstName, lastName, email and phone. The patient externalId and owner.externalId are required so records can be de-duplicated and linked.
Query parameters
| qstringrequired | Free-text query — patient name, owner name, phone, or email. |
Response · 200
{
"patients": [
{
"externalId": "pat_771",
"name": "Sky",
"speciesKey": "1",
"breed": "Golden Retriever",
"color": "Golden",
"sex": "female",
"reproductiveStatus": "spayed",
"dateOfBirth": "2023-06-08",
"weight": { "value": 11, "unit": "kg" },
"owner": {
"externalId": "cli_553",
"firstName": "Peggy",
"lastName": "Bartl",
"email": "[email protected]",
"phone": "+15134041567"
}
}
]
} Upcoming appointments you implement
Powers the refresh button in Coggo (on-demand reconciliation — there is no cron). Return all appointments starting within the next days days, each with its embedded client and patient. Coggo runs them through the same idempotent pipeline as webhooks, healing anything a missed webhook left behind. Max 100 processed per refresh; refresh is debounced to once per minute.
Query parameters
| daysinteger | Look-ahead window. Default 7, max 30. |
Response · 200
{
"appointments": [
{
"appointment": {
"externalId": "apt_1029",
"startsAt": "2026-06-12T14:00:00",
"timezone": "America/New_York",
"durationMinutes": 30,
"reason": "Lameness re-check",
"externalStaffId": "stf_44",
"status": "scheduled"
},
"client": {
"externalId": "cli_553",
"firstName": "Peggy",
"lastName": "Bartl",
"email": "[email protected]",
"phone": "+15134041567",
"addressLine1": "5038 Appaloosa Circle",
"addressLine2": "",
"city": "Columbus",
"stateRegion": "OH",
"postalCode": "43004"
},
"patient": {
"externalId": "pat_771",
"name": "Sky",
"speciesKey": "1",
"breed": "Golden Retriever",
"color": "Golden",
"sex": "female",
"reproductiveStatus": "spayed",
"dateOfBirth": "2023-06-08",
"weight": { "value": 11, "unit": "kg" },
"externalClientId": "cli_553"
}
}
]
} Receive SOAP notes you implement
Coggo POSTs a signed envelope when a vet pushes a finalized SOAP note. Verify the signature headers exactly as Coggo verifies yours. Any 2xx marks the push sent; anything else is retried 5× with backoff and surfaces in the clinic's integration dashboard.
Envelope fields
| meta.externalPatientIdstring | Your patient id (from the id mapping). Null if the patient was created in Coggo and never linked. |
| meta.externalAppointmentIdstring | Your appointment id when the note belongs to a synced visit. |
| meta.noteId / meta.versionstring / int | Treat (noteId, version) as your idempotency key — amended notes re-push with version+1. |
| formats.jsonobject | Structured SOAP, schemaVersion 1.0. Each section (subjective / objective / assessment / plan) is { title, fields[] }; read each field's label and value (a field with no data has value "Not mentioned" or ""). Plus templateExtension for structured templates (grids, charts). |
| formats.json.vitalsobject | Present only when at least one vital was captured (the node is omitted otherwise). Flat keys (weight, temperature {value, unit, raw}, heartRate, …) + confidence. An empty string means that vital was not captured. |
| formats.textobject | Plain-text SOAP split per section (subjective / objective / assessment / plan) so each can be its own EMR node. Only sections with data are present; narrative-only notes return narrative. |
| formats.htmlstring | Compact HTML fragment (no doctype/head/body) for embedding as an EMR record node. |
Consume whichever format fits your system — most PIMSes store html for display and json for data.
Request you'll receive
{
"id": "push_a1b2c3",
"occurredAt": "2026-06-12T15:04:05Z",
"meta": {
"externalPatientId": "pat_771",
"externalAppointmentId": "apt_1029",
"vet": { "name": "Tom Malloy",
"externalStaffId": "stf_44" },
"noteId": "d3f1...",
"version": 1,
"signedAt": "2026-06-12T15:03:40Z",
"language": "en"
},
"formats": {
"json": {
"schemaVersion": "1.0",
"objective": {
"title": "Objective / Physical Exam",
"fields": [
{ "name": "temperature", "label": "Temperature (Rectal)",
"type": "number_with_unit", "unit": "°C", "value": 36.89 },
{ "name": "heart_rate", "label": "Heart Rate",
"type": "number_with_unit", "unit": "bpm", "value": "Not mentioned" }
]
},
"subjective": { "title": "Subjective / History", "fields": [ ... ] },
"assessment": { "title": "Assessment / Diagnosis", "fields": [ ... ] },
"plan": { "title": "Plan", "fields": [ ... ] },
// present only when vitals were captured:
"vitals": {
"temperature": { "value": 36.89, "unit": "celsius", "raw": 98.4 },
"weight": "", "heartRate": "", "confidence": "low"
}
},
"text": {
"subjective": "Chief Complaint: Wellness / Checkup",
"objective": "Temperature (Rectal): 36.89 °C"
},
"html": "<!doctype html>..."
}
} Your response
HTTP/1.1 200 OK // any 2xx = recorded as sent
Errors & limits
| 400 | Validation failed — error names the exact field. |
| 401 | Invalid API key, or (when signature headers are sent) an invalid/expired signature. |
| 402 | integration_paused — the integration is paused or the clinic's plan no longer includes the Partner API. |
| 429 | Over 120 requests/min for this key. Honor Retry-After. |
All errors share one shape. Webhook processing failures still return 200 — the event is stored and retryable from the clinic's dashboard.
Error shape
{
"error": "integration_paused",
"detail": "optional human-readable context"
}