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.
- Base URL —
https://api.garisea.com - Format — JSON over HTTPS
- Versioning — the version is in the path (
/api/v1/…). We will not make breaking changes within a version.
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:
| Prefix | Environment |
|---|---|
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_…
| Response | Meaning |
|---|---|
401 | Missing, malformed, or unrecognised key |
403 | Key is valid but not allowed (revoked, wrong scope, plan lacks API access) |
429 | Rate limit exceeded — see Rate limits |
503 | We 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
| Scope | Grants |
|---|---|
inventory:read | List your vehicles |
inventory:write | Update price, mileage, status |
leads:read | List your leads |
leads:write | Update lead status |
analytics:read | Read 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
| Query | Notes |
|---|---|
limit | Default 50, max 200 |
cursor | nextCursor from the previous page |
status | e.g. active, sold |
branchId | Restrict to one showroom |
updatedSince | ISO-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).
| Field | Notes |
|---|---|
listingPrice | Non-negative number |
mileage | Non-negative number |
status | active or sold only |
priceVisible | false 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
| Event | Fires when |
|---|---|
lead.created | A new lead is routed to you |
lead.updated | A lead's status changes |
vehicle.sold | A listing is marked sold |
vehicle.published | A 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.
429and5xx→ retried400,401,403,404,410,422→ not retried. You're telling us the request is wrong or the endpoint is gone- After the last attempt we mark it exhausted and notify you in the dashboard
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 |
| Writes | 120 |
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:
- No buyer contact details (see Leads).
- No listing creation. New stock arrives via bulk import or the dashboard,
which enforce the photo minimum and moderation rules.
PATCHupdates existing stock. - No GraphQL. Our GraphQL schema is an internal contract that changes freely; freezing it for external consumers would slow both of us down.
- No cross-account access, ever, under any key.
- No parts or service catalogue. Vehicles are the only inventory Garisea holds; parts, service and finance are handled as routed enquiries.
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.