Skip to main content

For providers

Connect your own system to Ventcare

If you already run practice-management, diary or records software, it can talk to Ventcare directly. Create a key under Integrations in your provider dashboard, then read your bookings, mark visits as attended or completed, and receive a signed message the moment anything changes.

Quick start

  1. 1. Sign in and open Integrations in your provider dashboard. Not listed yet? Create your listing first.
  2. 2. Create a named key and copy it — it is shown once.
  3. 3. Call the bookings endpoint below to confirm the key works.
  4. 4. Add your https endpoint, press Send test, and verify the signature.
  5. 5. Go live — write visit statuses back to Ventcare so clients see the same history you do.
curl "https://ventcare.org/api/public/v1/bookings?limit=5" \
  -H "Authorization: Bearer vc_live_your_key"

Authentication

Every request carries your key as a bearer token over https. We store only a one-way hash of the key, so nobody at Ventcare can read it back to you. A key reaches exactly one listing — yours — and can be revoked instantly. Base URL: https://ventcare.org. All responses are JSON and never cached.

Authorization: Bearer vc_live_9f3c…      # required on every call
Accept: application/json
Content-Type: application/json          # on POST only

Keep keys on your server. Never put one in a browser, mobile app or shared spreadsheet. If a key leaks, revoke it in Integrations and create a new one — existing bookings are unaffected.

Permissions

Each key carries a set of permissions. Requesting an endpoint without its permission returns 403 — nothing is partially applied.

PermissionAllows
listing:readRead your listing summary.
services:readRead your session types, prices and lengths.
availability:readRead your weekly hours and blocked periods.
bookings:readRead your bookings and their clients.
bookings:writeChange a visit's status.

Endpoints

Successful replies are wrapped in { data: … }. Ventcare's own fee, payout and tax figures are never returned.

GET/api/public/v1/listinglisting:read

Your listing as clients see it — name, city, status, verification, rating and headline price.

Response

{
  "data": {
    "id": "6f1c…", "name": "Nuru Counselling", "type": "therapist",
    "headline": "Trauma-informed therapy in Nairobi",
    "city": "Nairobi", "status": "approved", "verified": true,
    "rating": 4.8, "review_count": 21, "price_per_session": 3500,
    "specialties": ["Anxiety", "Trauma"],
    "languages": ["English", "Kiswahili"],
    "session_modes": ["in_person", "online"]
  }
}

GET/api/public/v1/servicesservices:read

Every session type on your listing, with price in KES and length in minutes.

Response

{
  "data": [
    {
      "id": "b2a9…", "name": "Individual therapy (50 min)",
      "description": "One-to-one session.", "price": 3500,
      "duration_minutes": 50, "category": "therapy", "is_active": true
    }
  ]
}

GET/api/public/v1/availabilityavailability:read

Your weekly opening hours plus any current or future blocked periods, so an external diary can mirror them.

Response

{
  "data": {
    "hours": [
      { "id": "…", "day_of_week": 1, "start_time": "09:00", "end_time": "17:00" }
    ],
    "blocks": [
      { "id": "…", "starts_at": "2026-10-02T06:00:00Z",
        "ends_at": "2026-10-02T12:00:00Z", "reason": "Away" }
    ]
  }
}

GET/api/public/v1/bookingsbookings:read

Your bookings, earliest appointment first. Filter with from, to, status and limit (default 50, maximum 200).

Request

GET /api/public/v1/bookings?from=2026-09-01T00:00:00Z&status=confirmed&limit=100

Response

{
  "data": [
    {
      "id": "d41f…", "listing_id": "6f1c…",
      "service": { "id": "b2a9…", "name": "Individual therapy (50 min)",
                   "duration_minutes": 50 },
      "scheduled_at": "2026-09-24T11:00:00Z",
      "status": "confirmed", "payment_status": "paid",
      "amount": 3500, "currency": "KES",
      "session_mode": "online", "notes": null,
      "client": { "name": "A. Wanjiru", "phone": "+2547…",
                  "email": "client@example.com" },
      "created_at": "2026-09-20T08:14:00Z"
    }
  ]
}

GET/api/public/v1/bookings/{id}bookings:read

A single booking on your listing. Returns 404 if the booking belongs to someone else.

Response

{ "data": { "id": "d41f…", "status": "confirmed", … } }

POST/api/public/v1/bookings/{id}/statusbookings:write

Move a visit through its lifecycle: pending, confirmed, completed or cancelled. Money, payment state and client ownership are never writable.

Request

POST /api/public/v1/bookings/d41f…/status
Content-Type: application/json

{ "status": "completed" }

Response

{ "data": { "id": "d41f…", "status": "completed", … } }

Webhooks

Register one https endpoint in your dashboard and we POST a small JSON body whenever a booking changes. Reply with any 2xx status within ten seconds; anything else is retried up to five times with growing gaps, and every attempt is listed in your dashboard.

EventSent when
booking.createdA client has chosen a time with you and started checkout.
booking.paidTheir M-Pesa payment cleared. Client phone and email are now included.
booking.updatedThe visit was rescheduled or its status changed.
booking.cancelledThe visit was cancelled by the client, by you, or by Ventcare.
booking.completedThe visit was marked as attended and finished.
pingYou pressed “Send test” in your dashboard.

Headers and body

POST https://your-system.example/ventcare
x-ventcare-event: booking.paid
x-ventcare-timestamp: 1737400000
x-ventcare-signature: hex(hmac_sha256(secret, timestamp + "." + rawBody))

{
  "event": "booking.paid",
  "created_at": "2026-09-21T11:02:31.204Z",
  "data": {
    "booking": {
      "id": "d41f…", "scheduled_at": "2026-09-24T11:00:00Z",
      "status": "confirmed", "payment_status": "paid",
      "amount": 3500, "currency": "KES",
      "client": { "name": "A. Wanjiru", "phone": "+2547…",
                  "email": "client@example.com" }
    }
  }
}

Verifying a call

Recompute the signature over the raw body before parsing it, compare in constant time, and reject anything with a timestamp older than five minutes.

// Node.js
import { createHmac, timingSafeEqual } from "crypto";

function verify(rawBody, headers, secret) {
  const ts = headers["x-ventcare-timestamp"];
  const sent = headers["x-ventcare-signature"];
  if (!ts || !sent) return false;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const expected = createHmac("sha256", secret)
    .update(`${ts}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(sent, "hex");
  const b = Buffer.from(expected, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}
# PHP
$ts   = $_SERVER['HTTP_X_VENTCARE_TIMESTAMP'] ?? '';
$sent = $_SERVER['HTTP_X_VENTCARE_SIGNATURE'] ?? '';
$raw  = file_get_contents('php://input');

if (abs(time() - (int) $ts) > 300) { http_response_code(400); exit; }
$expected = hash_hmac('sha256', $ts . '.' . $raw, $secret);
if (!hash_equals($expected, $sent)) { http_response_code(401); exit; }

http_response_code(200);

The same booking event can arrive more than once after a retry. Treat the booking id plus event as the key and make your handler idempotent. Rotating your signing secret in Integrations invalidates the old one immediately.

Errors & limits

Up to 120 calls a minute per key. Errors always look the same:

{ "error": { "code": "forbidden",
             "message": "This key does not have the \"bookings:write\" permission." } }
StatusCodeMeaning
400invalid_requestThe body or a query parameter was not accepted. The message says which.
401unauthorizedThe key is missing, malformed or revoked.
403forbiddenThe key is valid but lacks the permission this endpoint needs.
404not_foundThe record does not exist, or belongs to another listing.
429rate_limitedMore than 120 calls in a minute. Back off and retry.
503unavailableA temporary Ventcare-side fault. Safe to retry.

Retry 429 and 503 with exponential backoff. Never retry 401 or 403 — fix the key first. For diary sync, poll the bookings endpoint no more than once a minute and prefer webhooks for anything time-sensitive.

Versioning & support

The version lives in the path (/api/public/v1/). We only add fields and endpoints within a version; anything that would break existing integrations goes into a new one, and we write to every provider with an active key before that happens. A machine-readable description is available at /api/public/v1/openapi.json.

Questions, or need an endpoint that isn't here? Email sales@ventcare.org or use the contact form and tell us what your system needs.

Integration questions