Features Pricing
Open App
SafiTrack API Reference

API Reference

v1  ·  REST / JSON

The SafiTrack REST API gives you programmatic access to companies, contacts, visits, route plans, opportunities, and webhooks. All requests and responses use JSON. The API follows RESTful conventions and returns standard HTTP status codes.

Introduction

All API calls are made to the base URL below. Every authenticated endpoint requires two headers: your user JWT and the public anon key.

Base URL https://ndrkncirkekpqjjkasiy.supabase.co/rest/v1

Requests without valid credentials return 401 Unauthorized. If your token has expired, exchange it using the /auth/v1/token?grant_type=refresh_token endpoint before retrying.

Authentication

SafiTrack uses JWT bearer tokens issued on sign-in. Pass both the Authorization and apikey headers on every request.

curl
curl https://ndrkncirkekpqjjkasiy.supabase.co/rest/v1/companies \
  -H "Authorization: Bearer <USER_JWT>" \
  -H "apikey: <ANON_KEY>" \
  -H "Content-Type: application/json"
Token expiry

JWTs expire after 1 hour. Refresh tokens using POST /auth/v1/token?grant_type=refresh_token with {"refresh_token":"<TOKEN>"} in the body. Store and rotate refresh tokens securely.

Rate Limits

The API allows up to 1,000 requests per minute per access token. When the limit is exceeded the server responds with 429 Too Many Requests and includes a Retry-After header indicating how many seconds to wait before retrying.

http
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json

{"error":"rate_limited","message":"Quota exceeded. Retry after 30 s."}

Pagination

List endpoints support offset-based pagination via query parameters and return a Content-Range header indicating the total record count.

Parameter Type Default Description
limit integer 20 Max records to return (1–100)
offset integer 0 Number of records to skip
order string created_at.desc Field and direction, e.g. name.asc
http
GET /companies?limit=25&offset=50&order=name.asc HTTP/1.1

200 OK
Content-Range: 50-74/312

Errors

All errors return a consistent JSON body with error, message, and status fields.

json
{
  "error": "not_found",
  "message": "No company found with id c_abc123",
  "status": 404
}
Status Code Description
400 validation_error Request body is missing required fields or has invalid values
401 unauthorized Missing or expired JWT / apikey
403 forbidden Token valid but lacks permission for this resource
404 not_found Resource with the given ID does not exist
409 conflict Duplicate unique field (e.g. company name)
429 rate_limited Request quota exceeded
500 server_error Unexpected server-side failure

Companies /companies

Represents a business entity your team sells to or services. Every contact, visit, and opportunity is linked to a company.

GET /companies List companies

Query parameters

Parameter Type Description
limit optional integer Max records (default 20, max 100)
offset optional integer Records to skip (default 0)
name optional string Filter by company name (case-insensitive, partial match)
owner_id optional uuid Filter by assigned owner user id
industry optional string Filter by industry tag

Response

json — 200 OK
[
  {
    "id": "c_01HZ3BXQK5VMA",
    "name": "Acme Corp",
    "industry": "retail",
    "phone": "+254712345678",
    "email": "info@acme.co.ke",
    "website": "https://acme.co.ke",
    "owner_id": "u_01HZ1A...",
    "address": "Westlands, Nairobi",
    "created_at": "2025-03-15T08:22:11Z",
    "updated_at": "2025-06-01T14:05:33Z"
  }
]
POST /companies Create a company

Body parameters

Parameter Type Description
name required string Company display name (max 200 chars)
industry optional string Industry tag, e.g. retail, fmcg, pharma
phone optional string E.164-formatted phone number
email optional string Primary contact email
website optional string Full URL including scheme
address optional string Street / area address
owner_id optional uuid Assigned team member. Defaults to calling user.

Example request

curl
curl -X POST https://ndrkncirkekpqjjkasiy.supabase.co/rest/v1/companies \
  -H "Authorization: Bearer <USER_JWT>" \
  -H "apikey: <ANON_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Corp",
    "industry": "retail",
    "phone": "+254712345678",
    "email": "info@acme.co.ke"
  }'

Response

json — 201 Created
{
  "id": "c_01HZ3BXQK5VMA",
  "name": "Acme Corp",
  "industry": "retail",
  "phone": "+254712345678",
  "email": "info@acme.co.ke",
  "owner_id": "u_01HZ1A...",
  "created_at": "2025-06-12T09:00:00Z",
  "updated_at": "2025-06-12T09:00:00Z"
}
GET /companies/{id} Retrieve a company

Path parameters

Parameter Type Description
id required string Company ID returned from list or create

Returns the full company object or 404 if it does not exist or the calling user lacks access.

PATCH /companies/{id} Update a company

Send only the fields you want to change. All fields are optional. Returns the updated company object.

curl
curl -X PATCH https://...supabase.co/rest/v1/companies/c_01HZ3BXQK5VMA \
  -H "Authorization: Bearer <USER_JWT>" \
  -H "apikey: <ANON_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"phone":"+254799000111","industry":"fmcg"}'
DELETE /companies/{id} Delete a company

Permanently deletes the company and cascades to its associated contacts, visits, and opportunities. Returns 204 No Content on success.

Irreversible

Deletion cannot be undone. All linked contacts, visits, and opportunities are permanently removed.

Contacts /contacts

Individual people at a company. Contacts can be linked to visits, opportunities, and reminders.

GET /contacts List contacts

Query parameters

Parameter Type Description
company_id optional uuid Return only contacts for this company
search optional string Full-text search on name, email, phone
limit optional integer Max records (default 20)
offset optional integer Records to skip (default 0)

Response

json — 200 OK
[
  {
    "id": "ct_01HZ4DKQP8WN",
    "company_id": "c_01HZ3BXQK5VMA",
    "first_name": "Amina",
    "last_name": "Waweru",
    "email": "amina@acme.co.ke",
    "phone": "+254701112233",
    "title": "Procurement Manager",
    "created_at": "2025-04-02T11:30:00Z"
  }
]
POST /contacts Create a contact

Body parameters

Parameter Type Description
company_id required uuid Company this contact belongs to
first_name required string First name
last_name optional string Last name
email optional string Email address
phone optional string E.164-formatted phone
title optional string Job title
GET /contacts/{id} Retrieve a contact

Returns the full contact object including their linked company details.

PATCH /contacts/{id} Update a contact

Update any subset of contact fields. Returns the updated contact object.

Visits /sales_visits

Sales and field visits recorded by reps. Each visit captures GPS coordinates, outcome, duration, and notes.

GET /sales_visits List visits

Query parameters

Parameter Type Description
company_id optional uuid Filter by company
rep_id optional uuid Filter by sales rep
outcome optional string One of sale, follow_up, no_contact
from optional ISO 8601 Filter visits on or after this datetime
to optional ISO 8601 Filter visits before this datetime

Response

json — 200 OK
[
  {
    "id": "v_01HZ8MNZR4KL",
    "company_id": "c_01HZ3BXQK5VMA",
    "rep_id": "u_01HZ1A...",
    "visited_at": "2025-06-10T10:15:00Z",
    "outcome": "sale",
    "duration_mins": 45,
    "latitude": -1.2921,
    "longitude": 36.8219,
    "notes": "Agreed on 50-unit order. Follow up with invoice.",
    "created_at": "2025-06-10T10:15:00Z"
  }
]
POST /sales_visits Log a visit

Body parameters

Parameter Type Description
company_id required uuid Company being visited
visited_at required ISO 8601 Visit start timestamp
outcome required string sale · follow_up · no_contact
latitude optional float GPS latitude
longitude optional float GPS longitude
duration_mins optional integer Visit length in minutes
notes optional string Free-text visit notes
PATCH /sales_visits/{id} Update a visit

Update outcome, notes, or duration of an existing visit. Location fields cannot be changed after creation.

Route Plans /route_plans

Ordered collections of stops for a rep's field day. Optionally auto-optimised for shortest-path traversal.

GET /route_plans List route plans

Query parameters

Parameter Type Description
rep_id optional uuid Filter by assigned rep
date optional date (YYYY-MM-DD) Plans scheduled for this date
status optional string planned · active · completed
POST /route_plans Create a route plan

Body parameters

Parameter Type Description
rep_id required uuid Rep this plan is assigned to
date required date Scheduled date (YYYY-MM-DD)
stops required array<uuid> Ordered array of company IDs
optimise optional boolean Reorder stops for minimum travel distance (default false)
curl
curl -X POST https://...supabase.co/rest/v1/route_plans \
  -H "Authorization: Bearer <USER_JWT>" \
  -H "apikey: <ANON_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "rep_id": "u_01HZ1A...",
    "date": "2025-07-21",
    "stops": ["c_01HZ3...", "c_02KL9...", "c_03MN0..."],
    "optimise": true
  }'
GET /route_plans/{id} Retrieve a route plan

Returns the route plan with its ordered stops array, each resolved to the full company object with name and coordinates.

PATCH /route_plans/{id} Update a route plan

Modify stops or status of a route plan. To mark a plan complete, set "status":"completed".

Opportunities /opportunities

Sales pipeline deals. Track value, stage progress, expected close date, and win probability.

GET /opportunities List opportunities

Query parameters

Parameter Type Description
company_id optional uuid Filter by company
stage optional string lead · qualified · proposal · negotiation · won · lost
owner_id optional uuid Filter by owner

Response

json — 200 OK
[
  {
    "id": "opp_01HZ9KLWQ2TR",
    "company_id": "c_01HZ3BXQK5VMA",
    "title": "Q3 Bulk Order",
    "value": 450000,
    "currency": "KES",
    "stage": "proposal",
    "probability": 65,
    "close_date": "2025-08-31",
    "owner_id": "u_01HZ1A...",
    "created_at": "2025-06-05T08:00:00Z"
  }
]
POST /opportunities Create an opportunity

Body parameters

Parameter Type Description
company_id required uuid Associated company
title required string Deal name
value optional number Monetary value
currency optional string ISO 4217 currency code (default KES)
stage optional string Pipeline stage (default lead)
probability optional integer Win probability 0–100
close_date optional date Expected close date (YYYY-MM-DD)
PATCH /opportunities/{id} Update an opportunity

Move an opportunity through stages or update its value. To close a deal: set "stage":"won" or "stage":"lost".

Webhooks /webhooks

Register HTTPS endpoints to receive real-time event notifications. SafiTrack signs every delivery with an HMAC-SHA256 signature so you can verify authenticity.

Events

Event Triggered when
company.created A new company is added
company.updated A company record is changed
contact.created A new contact is added
visit.completed A visit is logged with an outcome
opportunity.stage_changed A deal moves to a new stage
opportunity.won A deal is marked won
route_plan.started A rep begins their route plan
GET /webhooks List webhook subscriptions

Returns all registered webhooks for the authenticated workspace. The secret field is never returned after creation.

POST /webhooks Register a webhook

Body parameters

Parameter Type Description
url required string HTTPS endpoint to receive events
events required array<string> List of event names to subscribe to
description optional string Internal label for this subscription
curl
curl -X POST https://...supabase.co/rest/v1/webhooks \
  -H "Authorization: Bearer <USER_JWT>" \
  -H "apikey: <ANON_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/hooks/safitrack",
    "events": ["visit.completed", "opportunity.won"],
    "description": "CRM sync pipeline"
  }'

The response returns a one-time secret. Store it immediately — it will not be shown again.

Verifying signatures

Each webhook delivery includes an X-SafiTrack-Signature header. Verify it with HMAC-SHA256:

node.js
const crypto = require('crypto');

function verifySignature(rawBody, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(`sha256=${expected}`)
  );
}

// Express example
app.post('/hooks/safitrack', express.raw({ type: '*/*' }), (req, res) => {
  const sig = req.headers['x-safitrack-signature'];
  if (!verifySignature(req.body, sig, process.env.WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(req.body);
  console.log('Event:', event.type, event.data);
  res.sendStatus(200);
});
DELETE /webhooks/{id} Delete a webhook

Unregisters the webhook. No further events will be delivered to that URL. Returns 204 No Content.