For companies on Ploti for Business

The Ploti Data API

Your projects, plots, buyers, sales and payments, in your own systems: your website’s plot availability, your CRM’s reservations, your accounts’ receipts. Reads, safe writes and webhooks, with a test company to build against.

  • Every write follows your company’s own rules and approvals
  • Buyers’ personal details only with their own permission
  • Exports by hand stay free on every plan

Getting started

  1. Open Developers in Ploti for Business (directors, or a role given “Developers”).
  2. Make a test key. It reaches only your test company: a private copy with sample data that never texts anyone and is never billed.
  3. Make your first call.
curl https://api.ploti.co.ke/v1/plots?limit=2 \
  -H "Authorization: Bearer ploti_test_…"
{
  "data": [
    { "id": "6f1c…", "projectId": "a2d9…", "number": "A-001", "status": "available",
      "cashPrice": 850000, "areaM2": 464.5, "boundary": { "type": "Polygon", "coordinates": [[[36.96, -1.475], …]] },
      "updatedAt": "2026-10-28T08:15:02.113Z", "version": 2 },
    …
  ],
  "nextCursor": "MjAyNi0xMC0yOCAx…"
}

When it works, make a live key with only the scopes your system needs, and swap it in.

Keys and scopes

Send the key on every request as Authorization: Bearer <key>. Live keys start ploti_live_ and reach your company; test keys start ploti_test_ and reach only its test company.

  • Shown once. Ploti keeps only a hash. Store keys in your system’s secrets, never in a website’s or app’s code.
  • Made with a fresh code. A director confirms each new key with a code texted to their phone.
  • Acting as their maker. A key can never do more than the person who made it, and stops if they leave the team.
  • Narrowed further to one branch, a list of IP addresses, or an expiry date, and revoked in one click.
ScopeLets the key

Reading and syncing

Lists come a page at a time (up to 500) with a nextCursor; send it back as cursor for the next page, until it’s null.

To keep your copy up to date, remember the latest updatedAt you’ve seen and ask only for what changed after it:

GET /v1/sales?updated_since=2026-10-28T08:15:02.113Z&cursor=…

A payment changes its sale (and the sale its plot and buyer), so their updatedAt moves too. Projects deleted and plots withdrawn from sale (when a layout is replaced) are listed at GET /v1/removed?since=….

Buyers’ phone, ID and KRA numbers come back masked unless the key has buyers:personal.

Writing safely

  • Your rules apply. A write goes through the same checks as your team on screen: roles, branches, plan limits, approvals. A discount over your limit waits for a director, as it would in the console. Buyers are texted as usual.
  • Retry without doubling. Send an Idempotency-Key header with any write; sending the same key again returns the first answer instead of making the change twice.
  • No lost updates. Every record has a version. Send it as If-Match when you change a record; if someone changed it since, you get 412 version_conflict and can fetch it again.
  • Your references. Buyers, sales and payments take an externalRef (your system’s id), unique in your company, and can be found by it.
  • Nothing silently deleted. Sales are cancelled through a request, and stay on record.
curl -X POST https://api.ploti.co.ke/v1/sales/9b3e…/payments \
  -H "Authorization: Bearer ploti_live_…" \
  -H "Idempotency-Key: mpesa-SGT8ZQ41KD" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 85000, "method": "mpesa", "reference": "SGT8ZQ41KD", "paidOn": "2026-10-28", "externalRef": "RCPT-20931" }'

Webhooks

Add an endpoint under Developers and choose its events. Ploti sends each one as a JSON POST within a minute, and retries with growing gaps for about a day if your endpoint doesn’t answer with a 2xx. Every delivery, and its answer, is listed under Developers, where you can send it again.

{ "id": "evt_5c1f…", "type": "payment.received", "createdAt": "2026-10-28T09:02:11.540Z",
  "data": { "saleId": "9b3e…", "amount": 85000, "receipt": "RC-004812", "source": "paybill" } }

Events carry ids and a summary; fetch the record for the rest. Check each one came from Ploti with the Ploti-Signature header: t=<unix seconds>,v1=<hex>, where the hex is an HMAC-SHA256 of "<t>.<raw body>" with your endpoint’s signing secret.

// Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';

function fromPloti(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // older than 5 minutes
  const want = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return v1.length === want.length && timingSafeEqual(Buffer.from(v1), Buffer.from(want));
}
EventWhen

Limits and pricing

  • Free trial: try the API during your company’s free trial, up to 1,000 calls a month.
  • Plans: some plans include the Data API, with their own monthly calls. See pricing.
  • Data API add-on: for other plans, KES 4,900 a month for 25,000 calls a month and reads; write access KES 5,000 a month on top.
  • At the allowance your directors choose: pause the API until next month, or carry on and pay for each extra 10,000 calls on the next invoice. They’re texted at 80% and 100%.
  • Speed: up to 120 requests a minute for each key (429 with Retry-After beyond it).
  • Test keys never count and are never billed.

Errors

Errors come back with the HTTP status and a body like { "error": "plot_taken", "message": "Someone else has just reserved or sold this plot. Choose another." }. The error code stays the same; the message is for people.

StatusCodes you may see
400invalid_input, bad_cursor, over_balance, no_plan
401unauthenticated, key_revoked, key_expired
402api_not_enabled, api_read_only, billing_read_only, plan_limit
403missing_scope, forbidden, ip_not_allowed, not_verified
404not_found
409plot_taken, duplicate, duplicate_external_ref, duplicate_reference, pending, closed
412version_conflict
429rate_limited, api_limit

Reference

Made from the API itself, so it’s always current. The machine-readable description (OpenAPI 3.1) is at /v1/openapi.json, for Postman, Insomnia or code generators.

Loading the reference…