Garisea API — v1

Programmatic access to your dealership's inventory and leads, for connecting a DMS, CRM or BI tool.

This is a private integration API for subscribed accounts, not a public developer platform. A key only ever sees its own account's data.


1. Getting a key

In the Garisea dealer dashboard: Settings → Developer → New key.

Choose only the permissions your integration needs (see Scopes). When you create the key we show it once:

gsk_live_9Kx2mQ7pR4vT8wY1zA3bC5dE6fG0hJ2k

We store only a one-way hash, so we cannot show or recover it later. Copy it straight into your secret store. If it's lost, revoke the key and create another.

The prefix tells you which environment issued the key:

PrefixEnvironment
gsk_live_Production — real inventory, real leads
gsk_test_Staging — safe to experiment against

Both authenticate the same way. Keep them in separate secret stores: a staging key pointed at production simply fails to authenticate, but a production key pointed at staging will read and write your real data.

API access is included on some plans only. If the option isn't available, your plan doesn't include it yet — speak to your account manager.

2. Authentication

Send the key as a bearer token on every request:

Authorization: Bearer gsk_live_…
ResponseMeaning
401Missing, malformed, or unrecognised key
403Key is valid but not allowed (revoked, wrong scope, plan lacks API access)
429Rate limit exceeded — see Rate limits
503We couldn't verify the key right now. Retry with backoff

Error bodies name the reason so you can act on it without guessing:

{ "error": "This key is missing a required scope: inventory:write.",
  "reason": "missing_scope" }

Revoking a key takes effect within about a minute.

3. Scopes

ScopeGrants
inventory:readList your vehicles
inventory:writeUpdate price, mileage, status
leads:readList your leads
leads:writeUpdate lead status
analytics:readRead your analytics roll-ups

A request needs every scope the endpoint requires. Grant the minimum: a sync job that only publishes stock has no reason to hold leads:write.

4. Inventory

GET /api/v1/inventory — inventory:read

QueryNotes
limitDefault 50, max 200
cursornextCursor from the previous page
statuse.g. active, sold
branchIdRestrict to one showroom
updatedSinceISO-8601. Only rows changed since
curl -H "Authorization: Bearer $GARISEA_KEY" \
  "https://api.garisea.com/api/v1/inventory?limit=100&updatedSince=2026-07-01T00:00:00Z"
{
  "data": [
    {
      "id": "clr…", "vendorId": "clq…", "branchId": null,
      "status": "active", "listingPrice": "2320000", "priceVisible": true,
      "mileage": 48000, "condition": "used",
      "registrationNo": "KDA 123A", "vinNo": null,
      "yearOfManufacture": "2020", "brand": "Mazda", "model": "Atenza",
      "updatedAt": "2026-07-27T09:14:00.000Z"
    }
  ],
  "nextCursor": "clr…"
}

Prices are strings, not numbers. JSON numbers lose precision on money, and a rounded figure reconciles wrongly. Parse with a decimal type.

Paginate with the cursor, not an offset. Your stock changes while you walk it; offsets silently skip rows, cursors don't. Keep requesting until nextCursor is null.

PATCH /api/v1/inventory/:externalId — inventory:write

Keyed on your identifier, so you never store ours. Set externalId when you create stock (bulk import or the dashboard).

FieldNotes
listingPriceNon-negative number
mileageNon-negative number
statusactive or sold only
priceVisiblefalse renders "Price on request"
curl -X PATCH -H "Authorization: Bearer $GARISEA_KEY" \
  -H "content-type: application/json" \
  -d '{"status":"sold"}' \
  "https://api.garisea.com/api/v1/inventory/SKU-10482"

Deliberately narrow. Photos, descriptions and moderation state stay in the dashboard and bulk import, which own the listing-quality rules.

If your key spans several dealerships (a group key), add ?vendorId=… — the same SKU can exist under two members and we won't guess which you meant.

5. Leads

GET /api/v1/leads — leads:read

Filters: limit, cursor, status, branchId, createdSince.

{
  "data": [
    {
      "id": "clr…", "vendorId": "clq…", "branchId": null,
      "assignedStaffId": null, "leadType": "VEHICLE_INQUIRY",
      "status": "NEW", "score": 72, "rating": "HOT",
      "source": "search", "vehicleId": "clr…",
      "responseTimeMs": null, "createdAt": "2026-07-27T09:14:00.000Z"
    }
  ],
  "nextCursor": null
}

This endpoint returns no buyer contact details. Names, phone numbers and emails are personal data under the Kenya Data Protection Act and are released through the consented in-app flow, where the buyer has agreed to be contacted — not through a bulk export. You get everything needed to route, prioritise and report; you contact the buyer in Garisea.

PATCH /api/v1/leads/:id — leads:write

{ "status": "CONTACTED" }

Valid: NEW, CONTACTED, CONVERTED, LOST. Lets your CRM be the system of record for workflow. Scoring, routing and assignment stay with us.

6. Webhooks

Rather than polling, have us POST events to you as they happen.

Configure a URL in Settings → Developer on any key. It must be https://. We show a signing secret once when the webhook is first configured — you need it to verify requests.

Events

EventFires when
lead.createdA new lead is routed to you
lead.updatedA lead's status changes
vehicle.soldA listing is marked sold
vehicle.publishedA listing goes live

Request

POST /your-endpoint
content-type: application/json
x-garisea-timestamp: 1785312840
x-garisea-signature: 3f9a…
{ "event": "lead.created", "deliveryId": "clr…", "data": { "id": "clr…", "…": "…" } }

Verifying a request

Compute HMAC-SHA256 over `${timestamp}.${rawBody}` with your signing secret and compare, in constant time, to x-garisea-signature. Use the raw request body — re-serialising the JSON changes the bytes and the signature won't match.

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, headers, secret) {
  const ts = headers["x-garisea-timestamp"];
  const sig = headers["x-garisea-signature"];
  // Reject stale timestamps to prevent replay.
  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(expected);
  const b = Buffer.from(sig ?? "");
  return a.length === b.length && timingSafeEqual(a, b);
}

Retries

Reply 2xx promptly — we treat >10s as a failure. On any other outcome we retry 5 times over about 8.5 hours: after 1m, 5m, 30m, 2h, then 6h. That span is chosen to ride out an overnight outage without retrying into a second business day.

Deliveries are at-least-once. A network failure after you've committed means we retry something you already processed, so make your handler idempotent — the deliveryId is stable per delivery and is the natural de-duplication key.

Inspecting and replaying

GET /api/v1/webhooks/deliveries — recent attempts for the calling key, with status, attempt count, HTTP code and error. Filter by status or event.

POST /api/v1/webhooks/deliveries/:id/replay — re-queue a failed delivery after fixing your endpoint. Also available in the dashboard.

Both need only a valid key — no extra scope. A delivered event cannot be replayed (409): re-sending it would duplicate the record on your side.

7. Rate limits

Per key, per minute:

Limit
Reads (GET)600
Writes120

Over the limit returns 429 with a Retry-After header in seconds. Back off and retry — don't spin.

8. What this API does not do

Stated plainly so you can plan around it:

9. Support

[email protected], or open a ticket from your dashboard. Include the deliveryId or key prefix (gsk_live_9Kx2mQ7p…) — never the full key. If you ever send one by accident, revoke it immediately in Settings → Developer.

Have a dealer account? Create an API key. New to Garisea? Create an account.