PIMS Partner API

Bidirectional integration between your practice management system and Coggo, the AI veterinary scribe.

  1. PIMS → Coggo: send signed webhook events when appointments, clients or patients change. Coggo upserts the records and a scheduled visit appears for the vet.
  2. Coggo → PIMS: Coggo calls endpoints hosted by your PIMS for patient search, upcoming-appointment reconciliation, and to push signed SOAP notes back in json, text and html.

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-IdstringoptionalUnique request/event id. Also the idempotency key (recommended even without a signature).
X-Coggo-TimestampintegeroptionalUnix seconds. If sent, requests older or newer than 300s are rejected.
X-Coggo-SignaturestringoptionalBase64 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

GET /v1/partner/ping

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

okbooleanAlways true on success.
clinicIdstringThe Coggo clinic this key is scoped to.
providerstringProvider name set at creation, e.g. woovet.
scopesstring[]Granted scopes.
statusstringactive 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

POST /v1/partner/webhooks

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

appointment.createdappointment.updatedappointment.cancelledclient.updatedpatient.updatedpatient.merged

Body fields

idstringrequiredYour unique event id (idempotency key).
typestringrequiredOne of the event types above.
occurredAtdate-timerequiredWhen it happened in your system.
data.appointmentobjectexternalId, startsAt (local clinic time) + timezone (IANA), durationMinutes, reason, externalStaffId, status, visitType. Required for appointment events.
data.appointment.visitTypestringThe appointment's visit type by name — e.g. "Dental", "Wellness / Checkup", "Surgery". Coggo matches it case-insensitively against the clinic's visit types; a name the clinic doesn't have yet is created automatically and the visit assigned to it. Omit to leave the type unchanged (new visits default to "General").
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.clientobjectexternalId (the owner's id in your PIMS), firstName, lastName, email, phone, addressLine1, addressLine2, city, stateRegion, postalCode. Send it on client.updated, and embed it on appointment or patient.updated events to upsert the owner's demographics along with the patient (no callback needed). Omit it and send only patient.externalClientId to link an existing owner without changing their details.
patient fieldsobjectexternalId, 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); microchipId, notes (only overwrite when sent); externalClientId; status (active | inactive | deceased — send whenever it changes; deceased hides the patient from active searches in Coggo, omit to leave unchanged).
data.mergedIntoExternalIdstringFor patient.merged — the surviving patient's external id.

What Coggo does

Upserts client → patient → creates a scheduled visit. Records are matched and de-duplicated by your external ids (client.externalId, patient.externalId) — the source of truth Coggo maintains the mapping for. Coggo does not merge by email/phone/name, since those can be shared across households.

Update semantics (JSON merge-patch). On patient.updated, a field you omit keeps its stored value in Coggo; a field you send explicitly as null (or an empty string) is cleared. This applies to breed, color, sex, reproductiveStatus, microchipId and notes. Exceptions: dateOfBirth and weight are never cleared by null — send a corrected value instead.

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",
      "visitType": "Follow-up"
    },
    "client": {
      "externalId": "cli_553",
      "firstName": "Peggy",
      "lastName": "Bartl",
      "email": "peggy@example.com",
      "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" },
      "microchipId": "985141000123456",
      "notes": "Anxious at the vet; muzzle for nail trims.",
      "externalClientId": "cli_553"
    }
  }
}

Request · patient.updated

{
  "id": "evt_9a3d72",
  "type": "patient.updated",
  "occurredAt": "2026-07-08T10:15:00Z",
  "data": {
    "client": {
      "externalId": "cli_553",
      "firstName": "Peggy",
      "lastName": "Bartl",
      "email": "peggy.bartl@example.com",
      "phone": "+15134041567",
      "addressLine1": "5038 Appaloosa Circle",
      "addressLine2": "",
      "city": "Columbus",
      "stateRegion": "OH",
      "postalCode": "43004"
    },
    "patient": {
      "externalId": "pat_771",
      "name": "Sky",
      "speciesKey": "1",
      "weight": { "value": 11.4, "unit": "kg" },
      "microchipId": "985141000123456",
      "notes": "Weight recheck in 4 weeks.",
      "externalClientId": "cli_553"
    }
  }
}

Response · 200

{ "received": true }

// replayed event:
{ "received": true, "duplicate": true }

Species mapping you implement

GET {your base URL}/species

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[].keystringrequiredYour PIMS species id / foreign key. Echo this back as patient.speciesKey.
species[].labelstringHuman-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" }
}

Staff directory you implement

GET {your base URL}/staff

Return your PIMS staff/provider list so the clinic can map each Coggo team member to a PIMS staff account from a dropdown during setup — clinics don't know internal staff ids. After mapping, appointment webhooks carrying externalStaffId are assigned to the mapped vet in Coggo; unmapped ids fall back to the practice owner.

Called when the clinic opens the Staff mapping card. Respond within 5 seconds. Optional but strongly recommended — without it the clinic must type raw staff ids by hand.

Response · 200

staff[].idstringrequiredYour PIMS staff id — the SAME value you send as externalStaffId on appointment webhooks.
staff[].namestringDisplay name shown in the mapping dropdown.
staff[].rolestringOptional role/title (e.g. DVM, Technician) shown alongside the name.

Response · 200

{
  "staff": [
    { "id": "stf_44", "name": "Dr. Tom Molloy", "role": "DVM" },
    { "id": "stf_51", "name": "Dr. Jamie Baker", "role": "DVM" },
    { "id": "stf_60", "name": "Craig Hall", "role": "Technician" }
  ]
}

Then on appointments

"appointment": {
  "externalId": "APT-2201",
  "externalStaffId": "stf_44",
  "start": "2026-08-21T09:30:00Z"
}

Upcoming appointments you implement

GET {your base URL}/appointments?days=7

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.

No batching or pagination is needed on your side — return the whole window in one response. Coggo processes the 100 soonest appointments per refresh (a limit on our side) and reports any remainder as skipped. Webhooks are the primary sync mechanism; refresh only heals what a missed webhook left behind, so a window with 100+ unsynced appointments usually signals a webhook delivery problem rather than a need for a bigger refresh. Refresh is debounced to once per minute.

Query parameters

daysintegerLook-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",
        "visitType": "Follow-up"
      },
      "client": {
        "externalId": "cli_553",
        "firstName": "Peggy",
        "lastName": "Bartl",
        "email": "peggy.bartl@example.com",
        "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

POST {your registered push endpoint}

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

actionstringcreated = store a new record · updated = the note was edited and re-pushed — replace the record you stored for this note, never append a duplicate · deleted = the note was deleted in Coggo — remove (or mark deleted) your record. Deleted envelopes are slim: ids only, no formats.
meta.externalPatientIdstringYour patient id (from the id mapping). Null if the patient was created in Coggo and never linked.
meta.externalAppointmentIdstringYour appointment id when the note belongs to a synced visit.
meta.noteId / meta.versionstring / intnoteId is Coggo's stable id for the note — identical on every push of the same note; amended notes re-push with version+1. Locate existing records by refId (if you returned one) or noteId.
meta.refIdstringYour own record id for this note, echoed back if you replied { "refId": "…" } to an earlier push. Null when you never returned one — match on noteId instead.
formats.jsonobjectStructured 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.vitalsobjectPresent 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.textobjectPlain-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.htmlstringCompact 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",
  "action": "created",  // or "updated" / "deleted"
  "meta": {
    "externalPatientId": "pat_771",
    "externalAppointmentId": "apt_1029",
    "vet": { "name": "Tom Malloy",
             "externalStaffId": "stf_44" },
    "noteId": "d3f1...",
    "version": 1,
    "refId": null,  // your id, once you return one
    "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

// Optional JSON body on created/updated —
// your record id for this note. Coggo stores
// it and sends it back as meta.refId on later
// updates and deletes of the same note:
{ "refId": "rec_90817" }

Delete notice you'll receive

{
  "id": "push_x9y8z7",
  "occurredAt": "2026-08-27T09:12:00Z",
  "action": "deleted",
  "meta": {
    "noteId": "d3f1...",
    "refId": "rec_90817",
    "externalPatientId": "pat_771"
  }
}
// remove or mark deleted your record;
// reply 2xx even if it's already gone

Errors & limits

400Validation failed — error names the exact field.
401Invalid API key, or (when signature headers are sent) an invalid/expired signature.
402integration_paused — the integration is paused or the clinic's plan no longer includes the Partner API.
429Over 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"
}