Fifi API
Version 1 · OpenAPI 3.1 document
The Fifi API reads and changes a workspace's contacts, companies, deals, tasks, and notes, calls and meetings, with the same rules as the app, and reads its floor plans and seats and its service records.
Authenticate with an API key from Settings → API (owners and admins): Authorization: Bearer fifi_<prefix>_<secret>. A key reaches only its own workspace, acts as the member who created it, and can do only what its scopes allow.
Requests and responses are JSON. Money is in integer cents; dates are ISO 8601. Each key may make 120 requests a minute, and each workspace 600.
API keys
Owners and admins create keys in Settings → API. Each key has a name, an expiry (30 days, 90 days, 365 days, never) and access to each area: none, read, or read and write. A workspace can have 10 active keys.
contacts:read,contacts:write: contactscompanies:read,companies:write: companiesdeals:read,deals:write: dealstasks:read,tasks:write: tasksactivities:read,activities:write: notes, calls and meetingswebhooks:read,webhooks:write: webhooksfloor_plans:read: floor plans and seats (read only)service_records:read: service records (read only)loads:read: loads (read only)invoices:read: invoices and payments (read only)
The lookups take: GET /me, any key; GET /members, a read scope of contacts:read, companies:read, deals:read, tasks:read, activities:read or webhooks:read (floor_plans:read or service_records:read alone isn’t enough); GET /tags, contacts:read; GET /pipeline, deals:read.
A key is shown once, as fifi_<prefix>_<secret>. Fifi stores only a hash of it, so a lost key can’t be shown again: revoke it and create another. Settings → API lists each key’s prefix, scopes, creator, expiry and when and from where it was last used.
A key reaches only its own workspace and acts as the member who created it, within its scopes: it can do what they could in the app, and the activity log shows changes as made by them via the key. It stops working when it’s revoked or expires, or while the workspace is suspended or being deleted. If its creator leaves the workspace, is deactivated, or is no longer an owner or admin, the key is revoked for good: create a new one.
Authentication
Send the key in the Authorization header of every request. The API is for servers: it sends no CORS headers, never accepts cookies, and refuses a request that carries both a Fifi session cookie and a key. Keep keys out of browsers, apps and source code. Requests with missing or wrong keys are limited per IP address, so an integration that shares its address with abusive traffic can be refused (429) for up to a minute.
curl "https://app.fetchfifi.com/api/v1/me" \
-H "Authorization: Bearer $FIFI_API_KEY"Requests and responses
Bodies are JSON (Content-Type: application/json) of up to 256 KB, sent with a Content-Length (chunked bodies are refused). Fields use snake_case. Money is in integer cents, timestamps are ISO 8601 in UTC, and calendar dates are YYYY-MM-DD. A service record’s amounts are numbers with up to 3 decimals. A record comes back as { "data": { … } }.
Writes follow the app’s own rules: the same validation, permissions, uniqueness (a contact’s email, a company’s domain), timeline entries and activity log. PATCH changes only the fields you send; null clears an optional one. A successful PATCH always sets updated_at, even when it changes no value, so a sync by updated_since sees it. DELETE moves contacts, companies and deals to Trash.
Custom fields. Contacts, companies and deals carry custom_fields: every active custom field the workspace defined, by its key, with null when it’s empty (GET /custom-fields lists them, with their types and a choice’s options). A choice holds its option’s value, a checkbox true or null, a date YYYY-MM-DD. Send custom_fields on POST and PATCH as a key → value map: only the keys you send change, and null clears one; an unknown or archived key is a 400 naming custom_fields.<key>. Lists filter by up to 5 of them: cf.<key>=value, cf.<key>.gte and cf.<key>.lte (numbers and dates), and cf.<key>.set=true or false.
Every response has an X-Request-Id header: quote it if you contact us.
Paging and filters
Lists come back as { "data": [ … ], "next_cursor": "…" }, newest first (sort=created_at) or most recently changed first (sort=updated_at). Ask for up to 100 at a time with limit, and pass next_cursor back as cursor for the next page; it’s null on the last one. To sync changes, list with sort=updated_at and updated_since. Deleted records are never listed.
Errors
Errors come back as { "error": { "code", "message", "details" } }. For invalid input, details.fields says what’s wrong with each field. A record in another workspace is simply not found.
- 400
invalid_request- 400
invalid_json- 400
invalid_cursor- 400
cookie_not_allowed- 401
unauthorized- 403
insufficient_scope- 403
forbidden- 403
module_disabled- 403
sample_workspace- 404
not_found- 405
method_not_allowed- 409
conflict- 409
idempotency_key_in_use- 413
payload_too_large- 415
unsupported_media_type- 411
length_required- 422
rule_violation- 422
idempotency_key_reused- 429
rate_limited- 500
internal- 503
api_disabled
Idempotency
Send an Idempotency-Key header (any 1–255 visible ASCII characters, such as a UUID) with a POST so a retry can’t create the same record twice. A repeat with the same key and body within 24 hours returns the first response, with Idempotent-Replayed: true; the same key with a different body is refused (422). Only successful responses are kept, so a request that failed can be retried with the same key.
Rate limits
Each key may make 120 requests a minute, and all of a workspace’s keys together 600. Responses carry RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds). Over a limit, the API answers 429 with Retry-After.
Webhooks
Webhooks tell your app when something changes in a workspace, whether it was changed in Fifi or through the API, so you don’t have to poll. Owners and admins add up to 10 endpoints in Settings → API → Webhooks, or through the API with the webhooks:write scope (POST /webhooks to subscribe, DELETE /webhooks/{id} to unsubscribe, the REST hooks pattern Zapier uses). A key can subscribe only to events of areas it can read: contact.* needs contacts:read, floor_plan.* needs floor_plans:read, and so on.
An endpoint is an https:// URL on the standard port (443) with a public host name. Fifi looks the name up before every delivery and never sends to private, loopback, link-local or cloud metadata addresses (IPv4 or IPv6), and doesn’t follow redirects.
Events
contact.created- A contact was added (by hand, by import, through the API).
contact.updated- A contact changed: its fields, owner, lead status, Do Not Track, tags, or it came back from Trash.
contact.deleted- A contact was moved to Trash.
company.created- A company was added.
company.updated- A company changed, or came back from Trash.
company.deleted- A company was moved to Trash.
deal.created- A deal was added.
deal.updated- Anything about a deal changed, including its stage and status (sent with the events below too).
deal.stage_changed- A deal moved to another pipeline stage.
deal.won- A deal was closed as won.
deal.lost- A deal was closed as lost.
deal.deleted- A deal was moved to Trash.
task.created- A task was added.
task.updated- A task changed, including being completed or reopened.
task.completed- A task was completed (sent with task.updated).
task.deleted- A task was deleted.
activity.created- A note, call or meeting was logged on a contact, company or deal.
floor_plan.created- A floor plan was added.
floor_plan.updated- A floor plan was saved in the editor, renamed, moved up or down the list, or its background image was added, replaced or removed; also a plan a kennel or seat was moved away from, or whose kennel or seat was deleted.
floor_plan.deleted- A floor plan was deleted (the plan as it was just before).
load.created- A load was added.
load.updated- Anything about a load changed: its fields, carrier, stops (their arrival and departure times too) or status, or it came back from Trash (sent with the events below too).
load.status_changed- A load moved to another status (`changes.status` is the one before; sent with load.updated).
load.delivered- A load was marked delivered (sent with load.updated and load.status_changed).
load.deleted- A load was moved to Trash.
invoice.sent- An invoice was issued: sent by email or marked as sent (it has its number now). Its lines, notes and billing address aren't in the event; read them with GET /invoices/{id}.
invoice.voided- An issued invoice was voided (it keeps its number; `void_reason` says why).
invoice.paid- An invoice reached paid: its payments add up to its total (sent with payment.recorded).
payment.recorded- A payment was recorded for an invoice, or a refund (`kind` is `refund`; `amount_cents` is always positive). Its notes aren't in the event; read them with GET /payments.
payment.voided- A payment (or refund) was voided because it was entered in error; it stays on the invoice, struck through, and counts for nothing.
Send test event on the webhook's page sends a ping.
The request
Fifi sends a POST with a JSON body: { "id", "type", "created_at", "data": { "object", "changes" } }. object is the record as the API returns it (see the objects below), except for text people write as notes: a company’s notes, a deal’s lost_reason_note and a note’s, call’s or meeting’s body are left out, as are long-text custom fields (a change to one still raises *.updated, without listing it); fetch the record with the API if you need them. Email content and files are never sent. A floor plan event carries the plan as GET /floor-plans lists it, without its shapes and pods (fetch GET /floor-plans/{id} for them). On *.updated events (and the deal and task events sent with them), changes has the previous value of each field that changed (for custom_fields, only the keys that changed). A record restored from Trash arrives as *.updated without changes, as does a floor plan whose change its summary doesn’t show (its place in the list, or its background replaced). Bodies are at most 64 KB; a bigger one would carry only the record’s id and "truncated": true.
{
"id": "0199a8f3-0b1c-7d2e-8f30-4a5b6c7d8e9f",
"type": "contact.updated",
"created_at": "2026-10-04T14:03:00.000Z",
"data": {
"object": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c10",
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@acme.com",
"phone": null,
"address": null,
"title": "CTO",
"lead_status": "qualified",
"source": "referral",
"do_not_track": false,
"company_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c11",
"owner_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c12",
"tags": [],
"custom_fields": {
"preferred_contact": "email",
"renewal_date": "2026-11-30"
},
"created_at": "2026-10-01T14:03:00.000Z",
"updated_at": "2026-10-04T14:03:00.000Z"
},
"changes": {
"lead_status": "contacted",
"title": null,
"custom_fields": {
"renewal_date": null
}
}
}
}The headers:
Fifi-Event-Id: the event's ID, the same on every retry and redelivery.Fifi-Event-Type: the event, such asdeal.won.Fifi-Signature:t=<unix seconds>,v1=<signature>, where the signature is the hex HMAC-SHA256 of<t>.<raw body>with the endpoint's signing secret.
Verifying signatures
Each endpoint has its own signing secret (whsec_…), shown once when you add the endpoint or roll the secret. Check every request before acting on it: compute the signature over the raw body, compare it in constant time, and reject a timestamp more than 5 minutes away from now (a replay). After Roll secret, the old secret keeps working for 24 hours and the header carries two v1 signatures, one for each: accept either, then switch to the new secret.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body exactly as it arrived (a Buffer or string), before any JSON parsing.
// header: the Fifi-Signature header. secret: the endpoint's signing secret (whsec_…).
export function verifyFifiSignature(rawBody, header, secret, toleranceSeconds = 300) {
const parts = String(header ?? "").split(",").map((p) => p.trim().split("="));
const t = Number(parts.find(([k]) => k === "t")?.[1]);
// Reject anything older (or newer) than 5 minutes: a replayed request.
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest();
// During a secret roll there are two v1 signatures: either may match.
return parts.some(([k, v]) => k === "v1" && /^[0-9a-f]{64}$/.test(v ?? "") && timingSafeEqual(Buffer.from(v, "hex"), expected));
}import express from "express";
const app = express();
// The raw body: a parsed-and-reserialized JSON body won't match the signature.
app.post("/fifi-webhooks", express.raw({ type: "application/json" }), (req, res) => {
if (!verifyFifiSignature(req.body, req.get("Fifi-Signature"), process.env.FIFI_WEBHOOK_SECRET)) {
return res.status(400).send("bad signature");
}
const event = JSON.parse(req.body.toString("utf8"));
// Fifi may send an event more than once: skip ones you've already handled.
if (alreadyHandled(event.id)) return res.sendStatus(200);
queueForProcessing(event); // answer within 10 seconds; do slow work afterwards
res.sendStatus(200);
});Retries, order and duplicates
- Answer with any 2xx status within 10 seconds. Anything else, a redirect, a timeout or no connection is a failure. Fifi reads at most 4 KB of your response and ignores it.
- A failure is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours: 7 attempts over about 21 hours.
- Order isn’t guaranteed, and an event may arrive more than once. Deduplicate by
Fifi-Event-Id, and use the object’supdated_atto ignore an older state that arrives after a newer one. - Fifi sends at most 2 requests at a time and 60 a minute to each endpoint, so a big import (one
contact.createdper row) arrives over a while. - An endpoint stops for good when the member who added it leaves the workspace or is no longer an owner or admin, or when the API key that added it is revoked or expires. A retried
POST /webhooks(the sameIdempotency-Key) answerssecret: null: Fifi doesn’t keep the secret where it could be shown again. - An endpoint that has failed every delivery for 24 hours is turned off, and the person who added it is emailed. Turning it back on doesn’t resend what was missed; the webhook’s page lists the last 30 days of deliveries, with each one’s attempts, response status and time, and Redeliver sends any of them again.
Workspace
GET/me
This key and its workspace
Any valid key: its workspace, name, scopes and rate limits. A quick way to check a key works.
Needs: any key
Example
curl "https://app.fetchfifi.com/api/v1/me" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": {
"workspace": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c00",
"name": "CorporateMutt"
},
"key": {
"name": "Zapier",
"prefix": "k3x9q2ab",
"scopes": [
"contacts:read",
"contacts:write"
],
"expires_at": null
},
"rate_limit": {
"per_key_per_minute": 120,
"per_workspace_per_minute": 600
}
}
}GET/members
List members
Active members (up to 500), oldest first: their IDs are what owner_id and assignee_id take. Needs a read scope of contacts:read, companies:read, deals:read, tasks:read, activities:read or webhooks:read (floor_plans:read alone isn't enough).
Needs: a read scope of contacts:read, companies:read, deals:read, tasks:read, activities:read or webhooks:read
Example
curl "https://app.fetchfifi.com/api/v1/members" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": [
{
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c12",
"name": "Olive Owner",
"role": "owner"
}
],
"next_cursor": null
}GET/tags
List tags
The workspace's contact tags (up to 500), A→Z. Filter contacts by one with GET /contacts?tag_id=.
Needs: contacts:read
Example
curl "https://app.fetchfifi.com/api/v1/tags" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": [
{
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c13",
"name": "VIP",
"color": "primary"
}
],
"next_cursor": null
}GET/pipeline
Pipeline stages and statuses
The values a deal's stage, status and lost reason take.
Needs: deals:read
Example
curl "https://app.fetchfifi.com/api/v1/pipeline" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": {
"stages": [
{
"id": "lead",
"label": "Lead"
},
{
"id": "qualified",
"label": "Qualified"
},
{
"id": "proposal",
"label": "Proposal"
},
{
"id": "negotiation",
"label": "Negotiation"
}
],
"statuses": [
"open",
"won",
"lost"
],
"lost_reasons": [
{
"id": "price",
"label": "Price"
},
{
"id": "timing",
"label": "Timing"
},
{
"id": "competitor",
"label": "Competitor"
},
{
"id": "no_decision",
"label": "No decision"
},
{
"id": "other",
"label": "Other"
}
]
}
}Custom fields
GET/custom-fields
List custom fields
The workspace's active custom fields, by record type and in order: their keys are what a contact's, company's or deal's custom_fields hold (deal fields only while Deals is on). Needs a read scope of contacts:read, companies:read, deals:read, tasks:read, activities:read or webhooks:read.
Needs: a read scope of contacts:read, companies:read, deals:read, tasks:read, activities:read or webhooks:read
Query parameters
object"contact" | "company" | "deal"- Only this record type's fields.
Example
curl "https://app.fetchfifi.com/api/v1/custom-fields?object=company" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": [
{
"object": "company",
"key": "mc_number",
"label": "MC number",
"type": "text",
"help_text": "The carrier's FMCSA motor carrier number.",
"position": 0,
"options": [],
"remind_days_before": null
},
{
"object": "company",
"key": "insurance_expiry",
"label": "Insurance expiry",
"type": "date",
"help_text": null,
"position": 1,
"options": [],
"remind_days_before": 30
}
],
"next_cursor": null
}Contacts
The contact object
idstring (uuid)required- The record's ID.
first_namestringrequiredlast_namestringrequiredemailstring,nullrequired- Stored lowercase. Unique among the workspace's contacts.
phonestring,nullrequiredaddressstring,nullrequired- A home or service address (free text).
titlestring,nullrequired- Job title.
lead_status"new" | "contacted" | "qualified" | "unqualified"requiredsource"inbound" | "referral" | "outbound" | "event" | "import" | "other" or nullrequireddo_not_trackbooleanrequired- Email to this contact is sent without open tracking.
company_idstring (uuid) or nullrequired- Their company; null when none, or when it's deleted.
owner_idstring (uuid)required- Who owns the contact: a member's ID (see GET /members).
tagsarray of objectrequired- Up to 50 tags, oldest first.
custom_fieldsobjectrequired- Every active custom field by key (GET /custom-fields); null when empty. A choice gives its option's value, a checkbox true, a date YYYY-MM-DD.
created_atstring (ISO 8601 date-time)required- When it was created (ISO 8601, UTC).
updated_atstring (ISO 8601 date-time)required- When it last changed (ISO 8601, UTC).
GET/contacts
List contacts
Live contacts (never deleted ones), newest first, with the Contacts list's filters. Custom fields filter with up to 5 parameters: cf.<key> (equals; a choice by its option's value, a checkbox true or false), cf.<key>.gte and cf.<key>.lte (number and date fields) and cf.<key>.set=true|false.
Needs: contacts:read
Query parameters
limitintegerdefault 25- Records per page, 1–100 (default 25).
cursorstring- The
next_cursorof the previous page. Use it with the samesort. sort"created_at" | "updated_at"default "created_at"created_at: newest first (default).updated_at: most recently changed first.updated_sincestring (ISO 8601 date-time)- Only records changed at or after this time (ISO 8601, with a time zone).
owner_idstring (uuid)- Only contacts this member owns.
lead_status"new" | "contacted" | "qualified" | "unqualified"tag_idstring (uuid)- Only contacts with this tag.
company_idstring (uuid)- Only this company's contacts.
emailstring- Only the contact with this email (exact, case-insensitive).
Example
curl "https://app.fetchfifi.com/api/v1/contacts?lead_status=qualified&limit=2" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": [
{
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c10",
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@acme.com",
"phone": "+1 555 0100",
"address": "12 Analytical Way\nLondon",
"title": "CTO",
"lead_status": "qualified",
"source": "referral",
"do_not_track": false,
"company_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c11",
"owner_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c12",
"tags": [
{
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c13",
"name": "VIP"
}
],
"custom_fields": {
"gate_code": "4410",
"pets_on_property": true,
"property_type": "house"
},
"created_at": "2026-10-01T14:03:00.000Z",
"updated_at": "2026-10-02T09:30:00.000Z"
}
],
"next_cursor": null
}POST/contacts
Create a contact
Created as in the app: the same checks, a timeline entry, and an activity-log entry that names the key.
Needs: contacts:write · Answers 201 Created
Body (JSON)
first_namestringrequired- Required, 1–100 characters.
last_namestringrequired- Required, 1–100 characters.
emailstring or null- Unique among live contacts (409 if taken).
phonestring or nulladdressstring or nulltitlestring or null- Job title.
lead_status"new" | "contacted" | "qualified" | "unqualified"- Default
new. source"inbound" | "referral" | "outbound" | "event" | "import" | "other" or nullcompany_idstring (uuid) or null- A live company in this workspace.
owner_idstring (uuid)- An active member (default: the key's creator). Reassigning follows the app's rules.
custom_fieldsobject- Custom field values by key (GET /custom-fields). Only the keys sent change; null clears. Unknown or archived keys: 400.
Example
curl -X POST "https://app.fetchfifi.com/api/v1/contacts" \
-H "Authorization: Bearer $FIFI_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"first_name":"Ada","last_name":"Lovelace","email":"ada@acme.com","title":"CTO","lead_status":"qualified","company_id":"0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c11"}'{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c10",
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@acme.com",
"phone": "+1 555 0100",
"address": "12 Analytical Way\nLondon",
"title": "CTO",
"lead_status": "qualified",
"source": "referral",
"do_not_track": false,
"company_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c11",
"owner_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c12",
"tags": [
{
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c13",
"name": "VIP"
}
],
"custom_fields": {
"gate_code": "4410",
"pets_on_property": true,
"property_type": "house"
},
"created_at": "2026-10-01T14:03:00.000Z",
"updated_at": "2026-10-02T09:30:00.000Z"
}
}GET/contacts/{id}
Get a contact
Needs: contacts:read
Path
idstring (uuid)required- The record's ID (in the path).
Example
curl "https://app.fetchfifi.com/api/v1/contacts/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c10",
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@acme.com",
"phone": "+1 555 0100",
"address": "12 Analytical Way\nLondon",
"title": "CTO",
"lead_status": "qualified",
"source": "referral",
"do_not_track": false,
"company_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c11",
"owner_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c12",
"tags": [
{
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c13",
"name": "VIP"
}
],
"custom_fields": {
"gate_code": "4410",
"pets_on_property": true,
"property_type": "house"
},
"created_at": "2026-10-01T14:03:00.000Z",
"updated_at": "2026-10-02T09:30:00.000Z"
}
}PATCH/contacts/{id}
Update a contact
Send only the fields to change; null clears an optional field.
Needs: contacts:write
Path
idstring (uuid)required- The record's ID (in the path).
Body (JSON)
first_namestring- Required, 1–100 characters.
last_namestring- Required, 1–100 characters.
emailstring or null- Unique among live contacts (409 if taken).
phonestring or nulladdressstring or nulltitlestring or null- Job title.
lead_status"new" | "contacted" | "qualified" | "unqualified"- Default
new. source"inbound" | "referral" | "outbound" | "event" | "import" | "other" or nullcompany_idstring (uuid) or null- A live company in this workspace.
owner_idstring (uuid)- An active member (default: the key's creator). Reassigning follows the app's rules.
custom_fieldsobject- Custom field values by key (GET /custom-fields). Only the keys sent change; null clears. Unknown or archived keys: 400.
do_not_trackbooleanexpected_updated_atstring (ISO 8601 date-time)- Optional precondition: the
updated_atyou last read. If the record has changed since, nothing is changed and the answer is 409conflict(reasonstale).
Example
curl -X PATCH "https://app.fetchfifi.com/api/v1/contacts/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"lead_status":"contacted","phone":null}'{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c10",
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@acme.com",
"phone": null,
"address": "12 Analytical Way\nLondon",
"title": "CTO",
"lead_status": "contacted",
"source": "referral",
"do_not_track": false,
"company_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c11",
"owner_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c12",
"tags": [
{
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c13",
"name": "VIP"
}
],
"custom_fields": {
"gate_code": "4410",
"pets_on_property": true,
"property_type": "house"
},
"created_at": "2026-10-01T14:03:00.000Z",
"updated_at": "2026-10-02T09:30:00.000Z"
}
}DELETE/contacts/{id}
Delete a contact
Moves it to Trash, where an owner or admin can restore it for 30 days. As in the app, members delete contacts they own and owners and admins any; a key acts as its creator.
Needs: contacts:write
Path
idstring (uuid)required- The record's ID (in the path).
Example
curl -X DELETE "https://app.fetchfifi.com/api/v1/contacts/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c10",
"deleted": true
}
}Companies
The company object
idstring (uuid)required- The record's ID.
namestringrequireddomainstring,nullrequired- Email domain, e.g. acme.com. Unique among the workspace's companies.
website_urlstring,nullrequireddescriptionstring,nullrequiredindustrystring,nullrequiredphonestring,nullrequiredaddressstring,nullrequirednotesstring,nullrequiredowner_idstring (uuid)required- Who owns the company: a member's ID (see GET /members).
custom_fieldsobjectrequired- Every active custom field by key (GET /custom-fields); null when empty. A choice gives its option's value, a checkbox true, a date YYYY-MM-DD.
created_atstring (ISO 8601 date-time)required- When it was created (ISO 8601, UTC).
updated_atstring (ISO 8601 date-time)required- When it last changed (ISO 8601, UTC).
GET/companies
List companies
Live companies, newest first. Custom fields filter with up to 5 parameters: cf.<key> (equals; a choice by its option's value, a checkbox true or false), cf.<key>.gte and cf.<key>.lte (number and date fields) and cf.<key>.set=true|false.
Needs: companies:read
Query parameters
limitintegerdefault 25- Records per page, 1–100 (default 25).
cursorstring- The
next_cursorof the previous page. Use it with the samesort. sort"created_at" | "updated_at"default "created_at"created_at: newest first (default).updated_at: most recently changed first.updated_sincestring (ISO 8601 date-time)- Only records changed at or after this time (ISO 8601, with a time zone).
owner_idstring (uuid)- Only companies this member owns.
domainstring- Only the company with this domain (exact, lowercase).
Example
curl "https://app.fetchfifi.com/api/v1/companies?domain=acme.com" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": [
{
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c11",
"name": "Acme",
"domain": "acme.com",
"website_url": "https://acme.com/",
"description": "Makers of everything.",
"industry": "Manufacturing",
"phone": null,
"address": null,
"notes": null,
"owner_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c12",
"custom_fields": {
"mc_number": "MC-123456",
"insurance_expiry": "2026-11-30",
"trucks": 12
},
"created_at": "2026-09-28T10:00:00.000Z",
"updated_at": "2026-10-02T09:30:00.000Z"
}
],
"next_cursor": null
}POST/companies
Create a company
Created as in the app: the same checks, a timeline entry, and an activity-log entry that names the key.
Needs: companies:write · Answers 201 Created
Body (JSON)
namestringrequired- Required, 1–200 characters.
domainstring or null- An email domain like acme.com (normalized; filled from website_url when left empty). Unique among live companies (409 if taken).
website_urlstring or null- http(s) only; acme.com means https://acme.com.
descriptionstring or nullindustrystring or nullphonestring or nulladdressstring or nullnotesstring or nullowner_idstring (uuid)- An active member (default: the key's creator). Reassigning follows the app's rules.
custom_fieldsobject- Custom field values by key (GET /custom-fields). Only the keys sent change; null clears. Unknown or archived keys: 400.
Example
curl -X POST "https://app.fetchfifi.com/api/v1/companies" \
-H "Authorization: Bearer $FIFI_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Acme","website_url":"acme.com","industry":"Manufacturing"}'{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c11",
"name": "Acme",
"domain": "acme.com",
"website_url": "https://acme.com/",
"description": "Makers of everything.",
"industry": "Manufacturing",
"phone": null,
"address": null,
"notes": null,
"owner_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c12",
"custom_fields": {
"mc_number": "MC-123456",
"insurance_expiry": "2026-11-30",
"trucks": 12
},
"created_at": "2026-09-28T10:00:00.000Z",
"updated_at": "2026-10-02T09:30:00.000Z"
}
}GET/companies/{id}
Get a company
Needs: companies:read
Path
idstring (uuid)required- The record's ID (in the path).
Example
curl "https://app.fetchfifi.com/api/v1/companies/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c11",
"name": "Acme",
"domain": "acme.com",
"website_url": "https://acme.com/",
"description": "Makers of everything.",
"industry": "Manufacturing",
"phone": null,
"address": null,
"notes": null,
"owner_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c12",
"custom_fields": {
"mc_number": "MC-123456",
"insurance_expiry": "2026-11-30",
"trucks": 12
},
"created_at": "2026-09-28T10:00:00.000Z",
"updated_at": "2026-10-02T09:30:00.000Z"
}
}PATCH/companies/{id}
Update a company
Send only the fields to change; null clears an optional field.
Needs: companies:write
Path
idstring (uuid)required- The record's ID (in the path).
Body (JSON)
namestring- Required, 1–200 characters.
domainstring or null- An email domain like acme.com (normalized; filled from website_url when left empty). Unique among live companies (409 if taken).
website_urlstring or null- http(s) only; acme.com means https://acme.com.
descriptionstring or nullindustrystring or nullphonestring or nulladdressstring or nullnotesstring or nullowner_idstring (uuid)- An active member (default: the key's creator). Reassigning follows the app's rules.
custom_fieldsobject- Custom field values by key (GET /custom-fields). Only the keys sent change; null clears. Unknown or archived keys: 400.
expected_updated_atstring (ISO 8601 date-time)- Optional precondition: the
updated_atyou last read. If the record has changed since, nothing is changed and the answer is 409conflict(reasonstale).
Example
curl -X PATCH "https://app.fetchfifi.com/api/v1/companies/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"industry":"Robotics"}'{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c11",
"name": "Acme",
"domain": "acme.com",
"website_url": "https://acme.com/",
"description": "Makers of everything.",
"industry": "Robotics",
"phone": null,
"address": null,
"notes": null,
"owner_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c12",
"custom_fields": {
"mc_number": "MC-123456",
"insurance_expiry": "2026-11-30",
"trucks": 12
},
"created_at": "2026-09-28T10:00:00.000Z",
"updated_at": "2026-10-02T09:30:00.000Z"
}
}DELETE/companies/{id}
Delete a company
Moves it to Trash, where an owner or admin can restore it for 30 days. Its contacts stay, without a company until it's restored. As in the app, members delete companies they own and owners and admins any; a key acts as its creator.
Needs: companies:write
Path
idstring (uuid)required- The record's ID (in the path).
Example
curl -X DELETE "https://app.fetchfifi.com/api/v1/companies/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c11",
"deleted": true
}
}Deals
The deal object
idstring (uuid)required- The record's ID.
namestringrequiredvalue_centsintegerrequired- Value in US cents (integer).
stage"lead" | "qualified" | "proposal" | "negotiation"required- Pipeline stage (see GET /pipeline). A closed deal keeps its last stage.
status"open" | "won" | "lost"requiredlost_reason"price" | "timing" | "competitor" | "no_decision" | "other" or nullrequiredlost_reason_notestring,nullrequiredexpected_close_datestring (YYYY-MM-DD) or nullrequired- A calendar date, YYYY-MM-DD.
closed_atstring (ISO 8601 date-time) or nullrequiredcompany_idstring (uuid)requiredcontact_idsarray of string (uuid)required- Its live contacts (up to 20).
owner_idstring (uuid)required- Who owns the deal: a member's ID (see GET /members).
custom_fieldsobjectrequired- Every active custom field by key (GET /custom-fields); null when empty. A choice gives its option's value, a checkbox true, a date YYYY-MM-DD.
created_atstring (ISO 8601 date-time)required- When it was created (ISO 8601, UTC).
updated_atstring (ISO 8601 date-time)required- When it last changed (ISO 8601, UTC).
GET/deals
List deals
Live deals, open and closed, newest first. Custom fields filter with up to 5 parameters: cf.<key> (equals; a choice by its option's value, a checkbox true or false), cf.<key>.gte and cf.<key>.lte (number and date fields) and cf.<key>.set=true|false.
Needs: deals:read
Query parameters
limitintegerdefault 25- Records per page, 1–100 (default 25).
cursorstring- The
next_cursorof the previous page. Use it with the samesort. sort"created_at" | "updated_at"default "created_at"created_at: newest first (default).updated_at: most recently changed first.updated_sincestring (ISO 8601 date-time)- Only records changed at or after this time (ISO 8601, with a time zone).
owner_idstring (uuid)- Only deals this member owns.
stage"lead" | "qualified" | "proposal" | "negotiation"status"open" | "won" | "lost"company_idstring (uuid)- Only this company's deals.
contact_idstring (uuid)- Only deals with this contact.
Example
curl "https://app.fetchfifi.com/api/v1/deals?status=open&stage=proposal" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": [
{
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c20",
"name": "Acme renewal",
"value_cents": 1250000,
"stage": "proposal",
"status": "open",
"lost_reason": null,
"lost_reason_note": null,
"expected_close_date": "2026-11-30",
"closed_at": null,
"company_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c11",
"contact_ids": [
"0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c10"
],
"owner_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c12",
"custom_fields": {
"loads_per_week": 12
},
"created_at": "2026-10-01T15:00:00.000Z",
"updated_at": "2026-10-03T11:20:00.000Z"
}
],
"next_cursor": null
}POST/deals
Create a deal
Created open, as in the app: the same checks, a timeline entry, and an activity-log entry that names the key.
Needs: deals:write · Answers 201 Created
Body (JSON)
namestringrequired- Required, 1–200 characters.
value_centsintegerrequired- Value in US cents: an integer up to 2,000,000,000 ($20,000,000).
stage"lead" | "qualified" | "proposal" | "negotiation"- Default
lead. expected_close_datestring (YYYY-MM-DD) or null- YYYY-MM-DD, from 2000 to 2100.
company_idstring (uuid)required- A live company in this workspace.
contact_idsarray of string (uuid)required- 1–20 live contacts in this workspace.
owner_idstring (uuid)- An active member (default: the key's creator). Reassigning follows the app's rules.
custom_fieldsobject- Custom field values by key (GET /custom-fields). Only the keys sent change; null clears. Unknown or archived keys: 400.
Example
curl -X POST "https://app.fetchfifi.com/api/v1/deals" \
-H "Authorization: Bearer $FIFI_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Acme renewal","value_cents":1250000,"stage":"proposal","expected_close_date":"2026-11-30","company_id":"0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c11","contact_ids":["0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c10"]}'{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c20",
"name": "Acme renewal",
"value_cents": 1250000,
"stage": "proposal",
"status": "open",
"lost_reason": null,
"lost_reason_note": null,
"expected_close_date": "2026-11-30",
"closed_at": null,
"company_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c11",
"contact_ids": [
"0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c10"
],
"owner_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c12",
"custom_fields": {
"loads_per_week": 12
},
"created_at": "2026-10-01T15:00:00.000Z",
"updated_at": "2026-10-03T11:20:00.000Z"
}
}GET/deals/{id}
Get a deal
Needs: deals:read
Path
idstring (uuid)required- The record's ID (in the path).
Example
curl "https://app.fetchfifi.com/api/v1/deals/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c20",
"name": "Acme renewal",
"value_cents": 1250000,
"stage": "proposal",
"status": "open",
"lost_reason": null,
"lost_reason_note": null,
"expected_close_date": "2026-11-30",
"closed_at": null,
"company_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c11",
"contact_ids": [
"0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c10"
],
"owner_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c12",
"custom_fields": {
"loads_per_week": 12
},
"created_at": "2026-10-01T15:00:00.000Z",
"updated_at": "2026-10-03T11:20:00.000Z"
}
}PATCH/deals/{id}
Update, close or reopen a deal
Send only the fields to change. status closes (won, lost) or reopens a deal, with the same timeline entries as the app; a closed deal's stage can't change until it's reopened (422).
Needs: deals:write
Path
idstring (uuid)required- The record's ID (in the path).
Body (JSON)
namestring- Required, 1–200 characters.
value_centsinteger- Value in US cents: an integer up to 2,000,000,000 ($20,000,000).
stage"lead" | "qualified" | "proposal" | "negotiation"- Default
lead. expected_close_datestring (YYYY-MM-DD) or null- YYYY-MM-DD, from 2000 to 2100.
company_idstring (uuid)- A live company in this workspace.
contact_idsarray of string (uuid)- 1–20 live contacts in this workspace.
owner_idstring (uuid)- An active member (default: the key's creator). Reassigning follows the app's rules.
custom_fieldsobject- Custom field values by key (GET /custom-fields). Only the keys sent change; null clears. Unknown or archived keys: 400.
status"open" | "won" | "lost"wonorlostcloses an open deal;openreopens a closed one (clearing its lost reason).lost_reason"price" | "timing" | "competitor" | "no_decision" | "other"- Required with
status: lost. lost_reason_notestring or null- Required with
lost_reason: other. expected_updated_atstring (ISO 8601 date-time)- Optional precondition: the
updated_atyou last read. If the record has changed since, nothing is changed and the answer is 409conflict(reasonstale).
Example
curl -X PATCH "https://app.fetchfifi.com/api/v1/deals/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status":"lost","lost_reason":"timing"}'{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c20",
"name": "Acme renewal",
"value_cents": 1250000,
"stage": "proposal",
"status": "lost",
"lost_reason": "timing",
"lost_reason_note": null,
"expected_close_date": "2026-11-30",
"closed_at": "2026-10-04T16:00:00.000Z",
"company_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c11",
"contact_ids": [
"0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c10"
],
"owner_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c12",
"custom_fields": {
"loads_per_week": 12
},
"created_at": "2026-10-01T15:00:00.000Z",
"updated_at": "2026-10-03T11:20:00.000Z"
}
}DELETE/deals/{id}
Delete a deal
Moves it to Trash, where an owner or admin can restore it for 30 days. As in the app, members delete deals they own and owners and admins any; a key acts as its creator.
Needs: deals:write
Path
idstring (uuid)required- The record's ID (in the path).
Example
curl -X DELETE "https://app.fetchfifi.com/api/v1/deals/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c20",
"deleted": true
}
}Tasks
The task object
idstring (uuid)required- The record's ID.
titlestringrequireddue_datestring (YYYY-MM-DD) or nullrequired- A calendar date, YYYY-MM-DD.
priority"low" | "normal" | "high"requiredcompletedbooleanrequiredcompleted_atstring (ISO 8601 date-time) or nullrequiredassignee_idstring (uuid)required- Who it's for: a member's ID (see GET /members).
record_type"contact" | "company" | "deal" or nullrequired- The contact, company or deal it's about, if any.
record_idstring (uuid) or nullrequiredcreated_atstring (ISO 8601 date-time)required- When it was created (ISO 8601, UTC).
updated_atstring (ISO 8601 date-time)required- When it last changed (ISO 8601, UTC).
GET/tasks
List tasks
Live tasks, open and done, newest first.
Needs: tasks:read
Query parameters
limitintegerdefault 25- Records per page, 1–100 (default 25).
cursorstring- The
next_cursorof the previous page. Use it with the samesort. sort"created_at" | "updated_at"default "created_at"created_at: newest first (default).updated_at: most recently changed first.updated_sincestring (ISO 8601 date-time)- Only records changed at or after this time (ISO 8601, with a time zone).
assignee_idstring (uuid)- Only tasks for this member.
completed"true" | "false"false: open tasks only;true: done ones only.due_fromstring (YYYY-MM-DD)- Due on or after this date (YYYY-MM-DD).
due_tostring (YYYY-MM-DD)- Due on or before this date (YYYY-MM-DD).
record_type"contact" | "company" | "deal"record_idstring (uuid)- With record_type: only tasks about this record.
Example
curl "https://app.fetchfifi.com/api/v1/tasks?completed=false&due_to=2026-10-31" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": [
{
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c30",
"title": "Send the proposal",
"due_date": "2026-10-09",
"priority": "high",
"completed": false,
"completed_at": null,
"assignee_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c12",
"record_type": "deal",
"record_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c20",
"created_at": "2026-10-02T08:00:00.000Z",
"updated_at": "2026-10-02T08:00:00.000Z"
}
],
"next_cursor": null
}POST/tasks
Create a task
Created as in the app, with an activity-log entry that names the key.
Needs: tasks:write · Answers 201 Created
Body (JSON)
titlestringrequired- Required, 1–300 characters.
due_datestring (YYYY-MM-DD) or null- YYYY-MM-DD, from 2000 to 2100.
priority"low" | "normal" | "high"- Default
normal. assignee_idstring (uuid)- An active member (default: the key's creator).
record_type"contact" | "company" | "deal"- With record_id: the live contact, company or deal it's about.
record_idstring (uuid)
Example
curl -X POST "https://app.fetchfifi.com/api/v1/tasks" \
-H "Authorization: Bearer $FIFI_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"title":"Send the proposal","due_date":"2026-10-09","priority":"high","record_type":"deal","record_id":"0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c20"}'{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c30",
"title": "Send the proposal",
"due_date": "2026-10-09",
"priority": "high",
"completed": false,
"completed_at": null,
"assignee_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c12",
"record_type": "deal",
"record_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c20",
"created_at": "2026-10-02T08:00:00.000Z",
"updated_at": "2026-10-02T08:00:00.000Z"
}
}GET/tasks/{id}
Get a task
Needs: tasks:read
Path
idstring (uuid)required- The record's ID (in the path).
Example
curl "https://app.fetchfifi.com/api/v1/tasks/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c30",
"title": "Send the proposal",
"due_date": "2026-10-09",
"priority": "high",
"completed": false,
"completed_at": null,
"assignee_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c12",
"record_type": "deal",
"record_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c20",
"created_at": "2026-10-02T08:00:00.000Z",
"updated_at": "2026-10-02T08:00:00.000Z"
}
}PATCH/tasks/{id}
Update or complete a task
Send only the fields to change. Moving the due date moves a pending reminder with it, as in the app.
Needs: tasks:write
Path
idstring (uuid)required- The record's ID (in the path).
Body (JSON)
titlestring- Required, 1–300 characters.
due_datestring (YYYY-MM-DD) or null- YYYY-MM-DD, from 2000 to 2100.
priority"low" | "normal" | "high"- Default
normal. assignee_idstring (uuid)- An active member (default: the key's creator).
completedboolean- true completes the task (logged on its record's timeline); false reopens it.
Example
curl -X PATCH "https://app.fetchfifi.com/api/v1/tasks/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"completed":true}'{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c30",
"title": "Send the proposal",
"due_date": "2026-10-09",
"priority": "high",
"completed": true,
"completed_at": "2026-10-04T16:00:00.000Z",
"assignee_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c12",
"record_type": "deal",
"record_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c20",
"created_at": "2026-10-02T08:00:00.000Z",
"updated_at": "2026-10-02T08:00:00.000Z"
}
}DELETE/tasks/{id}
Delete a task
Deletes it, as in the app: its assignee, its creator or an owner or admin may (a key acts as its creator). Deleted tasks can't be restored.
Needs: tasks:write
Path
idstring (uuid)required- The record's ID (in the path).
Example
curl -X DELETE "https://app.fetchfifi.com/api/v1/tasks/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c30",
"deleted": true
}
}Activities
The activity object
idstring (uuid)required- The record's ID.
type"note" | "call" | "meeting"required- A note, a call or a meeting.
record_type"contact" | "company" | "deal"required- The contact, company or deal whose timeline it's on.
record_idstring (uuid)requiredtitlestring,nullrequired- Its optional title (the app's “subject”).
bodystringrequiredoccurred_atstring (ISO 8601 date-time)required- When it happened; a meeting's start (ISO 8601, UTC).
duration_minutesinteger or nullrequired- A meeting's length in minutes; null otherwise.
author_idstring (uuid) or nullrequiredcreated_atstring (ISO 8601 date-time)required- When it was created (ISO 8601, UTC).
updated_atstring (ISO 8601 date-time)required- When it last changed (ISO 8601, UTC).
GET/activities
List notes, calls and meetings
Live entries, newest first. Entries on a deleted record stay listed until it's purged, but can't be changed.
Needs: activities:read
Query parameters
limitintegerdefault 25- Records per page, 1–100 (default 25).
cursorstring- The
next_cursorof the previous page. Use it with the samesort. sort"created_at" | "updated_at"default "created_at"created_at: newest first (default).updated_at: most recently changed first.updated_sincestring (ISO 8601 date-time)- Only records changed at or after this time (ISO 8601, with a time zone).
record_type"contact" | "company" | "deal"record_idstring (uuid)- With record_type: only this record's entries.
type"note" | "call" | "meeting"author_idstring (uuid)- Only entries this member logged.
Example
curl "https://app.fetchfifi.com/api/v1/activities?record_type=contact&record_id=0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c10" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": [
{
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c40",
"type": "call",
"record_type": "contact",
"record_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c10",
"title": "Intro call",
"body": "Wants a quote for 12 seats by Friday.",
"occurred_at": "2026-10-03T15:30:00.000Z",
"duration_minutes": null,
"author_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c12",
"created_at": "2026-10-03T15:45:00.000Z",
"updated_at": "2026-10-03T15:45:00.000Z"
}
],
"next_cursor": null
}POST/activities
Log a note, call or meeting
Logged on a live contact, company or deal, as in the app, by the key's creator.
Needs: activities:write · Answers 201 Created
Body (JSON)
type"note" | "call" | "meeting"requiredrecord_type"contact" | "company" | "deal"requiredrecord_idstring (uuid)required- A live contact, company or deal in this workspace.
titlestring or null- Up to 200 characters; null clears it.
bodystringrequired- Required, 1–10,000 characters.
occurred_atstring (ISO 8601 date-time)- ISO 8601 with a time zone. Default: now. Only meetings can be in the future (up to two years).
duration_minutesinteger or null- Meetings only: 5 minutes to 24 hours.
Example
curl -X POST "https://app.fetchfifi.com/api/v1/activities" \
-H "Authorization: Bearer $FIFI_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"type":"call","record_type":"contact","record_id":"0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c10","title":"Intro call","body":"Wants a quote for 12 seats by Friday.","occurred_at":"2026-10-03T11:30:00-04:00"}'{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c40",
"type": "call",
"record_type": "contact",
"record_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c10",
"title": "Intro call",
"body": "Wants a quote for 12 seats by Friday.",
"occurred_at": "2026-10-03T15:30:00.000Z",
"duration_minutes": null,
"author_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c12",
"created_at": "2026-10-03T15:45:00.000Z",
"updated_at": "2026-10-03T15:45:00.000Z"
}
}GET/activities/{id}
Get a note, call or meeting
Needs: activities:read
Path
idstring (uuid)required- The record's ID (in the path).
Example
curl "https://app.fetchfifi.com/api/v1/activities/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c40",
"type": "call",
"record_type": "contact",
"record_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c10",
"title": "Intro call",
"body": "Wants a quote for 12 seats by Friday.",
"occurred_at": "2026-10-03T15:30:00.000Z",
"duration_minutes": null,
"author_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c12",
"created_at": "2026-10-03T15:45:00.000Z",
"updated_at": "2026-10-03T15:45:00.000Z"
}
}PATCH/activities/{id}
Edit a note, call or meeting
As in the app, only its author edits an entry (a key acts as its creator), and only a meeting's time and length can change.
Needs: activities:write
Path
idstring (uuid)required- The record's ID (in the path).
Body (JSON)
titlestring or null- Up to 200 characters; null clears it.
bodystring- Required, 1–10,000 characters.
occurred_atstring (ISO 8601 date-time)- ISO 8601 with a time zone. Default: now. Only meetings can be in the future (up to two years).
duration_minutesinteger or null- Meetings only: 5 minutes to 24 hours.
Example
curl -X PATCH "https://app.fetchfifi.com/api/v1/activities/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"body":"Wants a quote for 15 seats by Friday."}'{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c40",
"type": "call",
"record_type": "contact",
"record_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c10",
"title": "Intro call",
"body": "Wants a quote for 15 seats by Friday.",
"occurred_at": "2026-10-03T15:30:00.000Z",
"duration_minutes": null,
"author_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c12",
"created_at": "2026-10-03T15:45:00.000Z",
"updated_at": "2026-10-03T15:45:00.000Z"
}
}DELETE/activities/{id}
Delete a note, call or meeting
As in the app, its author or an owner or admin deletes an entry (a key acts as its creator). Deleted entries can't be restored.
Needs: activities:write
Path
idstring (uuid)required- The record's ID (in the path).
Example
curl -X DELETE "https://app.fetchfifi.com/api/v1/activities/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c40",
"deleted": true
}
}Webhook endpoints
The webhook endpoint object
idstring (uuid)required- The record's ID.
urlstringrequired- Where events are POSTed: HTTPS, port 443, a public host name.
descriptionstringrequired- Up to 200 characters; may be empty.
eventsarray of "contact.created" | "contact.updated" | "contact.deleted" | "company.created" | "company.updated" | "company.deleted" | "deal.created" | "deal.updated" | "deal.stage_changed" | "deal.won" | "deal.lost" | "deal.deleted" | "task.created" | "task.updated" | "task.completed" | "task.deleted" | "activity.created" | "floor_plan.created" | "floor_plan.updated" | "floor_plan.deleted" | "load.created" | "load.updated" | "load.status_changed" | "load.delivered" | "load.deleted" | "invoice.sent" | "invoice.voided" | "invoice.paid" | "payment.recorded" | "payment.voided"required- The events it receives.
status"active" | "disabled"requireddisabled: no deliveries are sent (turned off by an owner or admin, or by Fifi after 24 hours of failed deliveries).disabled_reason"failing" | "manual" | "creator_removed" | "key_revoked" or nullrequired- Why it's disabled:
failing(every delivery failed for 24 hours),manual,creator_removed(the member who added it left or is no longer an owner or admin) orkey_revoked(the API key that added it was revoked or expired). The last two can't be turned back on. created_atstring (ISO 8601 date-time)required- When it was created (ISO 8601, UTC).
updated_atstring (ISO 8601 date-time)required- When it last changed (ISO 8601, UTC).
GET/webhooks
List webhook endpoints
The workspace's endpoints (at most 10), oldest first. Secrets are never shown again.
Needs: webhooks:read
Example
curl "https://app.fetchfifi.com/api/v1/webhooks" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": [
{
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c30",
"url": "https://hooks.example.com/fifi",
"description": "Zapier: new contacts",
"events": [
"contact.created",
"contact.updated"
],
"status": "active",
"disabled_reason": null,
"created_at": "2026-10-04T12:00:00.000Z",
"updated_at": "2026-10-04T12:00:00.000Z"
}
],
"next_cursor": null
}POST/webhooks
Subscribe a webhook endpoint
Adds an endpoint (REST hooks: subscribe on activation, DELETE to unsubscribe). The response carries its signing secret, once. At most 10 per workspace (422 limit past that). Audited as made by the key's creator via the key.
Needs: webhooks:write · Answers 201 Created
Body (JSON)
urlstring (uri)required- An https:// URL on port 443 with a public host name (not an IP address).
descriptionstring- Up to 200 characters.
eventsarray of "contact.created" | "contact.updated" | "contact.deleted" | "company.created" | "company.updated" | "company.deleted" | "deal.created" | "deal.updated" | "deal.stage_changed" | "deal.won" | "deal.lost" | "deal.deleted" | "task.created" | "task.updated" | "task.completed" | "task.deleted" | "activity.created" | "floor_plan.created" | "floor_plan.updated" | "floor_plan.deleted" | "load.created" | "load.updated" | "load.status_changed" | "load.delivered" | "load.deleted" | "invoice.sent" | "invoice.voided" | "invoice.paid" | "payment.recorded" | "payment.voided"required- One or more events. Each needs the key to have its area's read scope (
contact.*needscontacts:read, and so on).
Example
curl -X POST "https://app.fetchfifi.com/api/v1/webhooks" \
-H "Authorization: Bearer $FIFI_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"url":"https://hooks.example.com/fifi","description":"Zapier: new contacts","events":["contact.created","contact.updated"]}'{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c30",
"url": "https://hooks.example.com/fifi",
"description": "Zapier: new contacts",
"events": [
"contact.created",
"contact.updated"
],
"status": "active",
"disabled_reason": null,
"created_at": "2026-10-04T12:00:00.000Z",
"updated_at": "2026-10-04T12:00:00.000Z",
"secret": "whsec_Q2h3n8pX0aB7cD1eF4gH6iJ9kL2mN5oP8qR1sT4uV7w"
}
}GET/webhooks/{id}
Get a webhook endpoint
Needs: webhooks:read
Path
idstring (uuid)required- The record's ID (in the path).
Example
curl "https://app.fetchfifi.com/api/v1/webhooks/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c30",
"url": "https://hooks.example.com/fifi",
"description": "Zapier: new contacts",
"events": [
"contact.created",
"contact.updated"
],
"status": "active",
"disabled_reason": null,
"created_at": "2026-10-04T12:00:00.000Z",
"updated_at": "2026-10-04T12:00:00.000Z"
}
}DELETE/webhooks/{id}
Unsubscribe a webhook endpoint
Deletes the endpoint and its delivery log. Nothing more is sent to it.
Needs: webhooks:write
Path
idstring (uuid)required- The record's ID (in the path).
Example
curl -X DELETE "https://app.fetchfifi.com/api/v1/webhooks/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c30",
"deleted": true
}
}Floor plans
The floor plan object
idstring (uuid)required- The record's ID.
namestringrequired- 1–60 characters, unique among the workspace's live plans in any case.
columnsintegerrequired- Grid width in cells (4–120).
rowsintegerrequired- Grid height in cells (4–120).
versionintegerrequired- Starts at 1 and goes up by one each time its layout or grid is saved, a kennel or seat leaves it, or it's deleted.
has_backgroundbooleanrequired- Whether it has a background image (the image itself isn't available through the API).
created_atstring (ISO 8601 date-time)required- When it was created (ISO 8601, UTC).
updated_atstring (ISO 8601 date-time)required- When it last changed (ISO 8601, UTC).
shapesarray of objectrequired- Up to 500, oldest first.
podsarray of objectrequired- The kennels placed on it (only while Boarding is on), then its seats (only while Online booking is on), each in their order.
GET/floor-plans
List floor plans
Live floor plans (at most 20 a workspace), newest first. Works while Boarding or Online booking is on.
Needs: floor_plans:read
Query parameters
limitintegerdefault 25- Records per page, 1–100 (default 25).
cursorstring- The
next_cursorof the previous page. Use it with the samesort. sort"created_at" | "updated_at"default "created_at"created_at: newest first (default).updated_at: most recently changed first.updated_sincestring (ISO 8601 date-time)- Only records changed at or after this time (ISO 8601, with a time zone).
Example
curl "https://app.fetchfifi.com/api/v1/floor-plans" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": [
{
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c40",
"name": "Salon",
"columns": 30,
"rows": 20,
"version": 4,
"has_background": false,
"created_at": "2026-10-02T08:00:00.000Z",
"updated_at": "2026-10-05T16:20:00.000Z"
}
],
"next_cursor": null
}GET/floor-plans/{id}
Get a floor plan
A live plan with its shapes and pods, in quarter cells as stored. Kennels' pods are included only while Boarding is on, and seats' pods only while Online booking is on.
Needs: floor_plans:read
Path
idstring (uuid)required- The record's ID (in the path).
Example
curl "https://app.fetchfifi.com/api/v1/floor-plans/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": {
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c40",
"name": "Salon",
"columns": 30,
"rows": 20,
"version": 4,
"has_background": false,
"created_at": "2026-10-02T08:00:00.000Z",
"updated_at": "2026-10-05T16:20:00.000Z",
"shapes": [
{
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c41",
"kind": "wall",
"label": "",
"x": 0,
"y": 0,
"w": 120,
"h": 1,
"rotation": 0
},
{
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c42",
"kind": "area",
"label": "Waiting area",
"x": 4,
"y": 56,
"w": 40,
"h": 20,
"rotation": 0
}
],
"pods": [
{
"kind": "kennel",
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c43",
"name": "Run 1",
"x": 64,
"y": 8,
"w": 12,
"h": 12,
"rotation": 0
},
{
"kind": "seat",
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c44",
"name": "Table 1",
"x": 8,
"y": 8,
"w": 12,
"h": 8,
"rotation": 90
}
]
}
}Seats
The seat object
idstring (uuid)required- The record's ID.
namestringrequired- 1–60 characters, unique among the workspace's live seats in any case.
kindstringrequired- What it is: Table, Tub, Chair, Dryer, Station or the workspace's own word (1–30 characters).
activebooleanrequired- false: switched off (it takes no new bookings; the ones it has keep it).
orderintegerrequired- Its place in the workspace's seat list (ascending).
floor_plan_idstring (uuid) or nullrequired- The live floor plan it's placed on; null when unplaced.
xinteger or nullrequired- Left edge of its footprint, in quarter cells; null when it isn't on a plan.
yinteger or nullrequired- Top edge of its footprint, in quarter cells; null when it isn't on a plan.
winteger or nullrequired- Width before rotation, in quarter cells; null when it isn't on a plan.
hinteger or nullrequired- Height before rotation, in quarter cells; null when it isn't on a plan.
rotation0 or 90 or 180 or 270 or nullrequired- Degrees clockwise; null when it isn't on a plan.
GET/seats
List seats
Live appointment seats (tables, tubs, chairs…; at most 100 a workspace), newest first, each with where it's placed. Seats belong to Online booking: while it's off (and Boarding on) the list is empty, as plans then leave seats out.
Needs: floor_plans:read
Query parameters
limitintegerdefault 25- Records per page, 1–100 (default 25).
cursorstring- The
next_cursorof the previous page. Use it with the samesort. sort"created_at" | "updated_at"default "created_at"created_at: newest first (default).updated_at: most recently changed first.updated_sincestring (ISO 8601 date-time)- Only records changed at or after this time (ISO 8601, with a time zone).
Example
curl "https://app.fetchfifi.com/api/v1/seats" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": [
{
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c44",
"name": "Table 1",
"kind": "Table",
"active": true,
"order": 0,
"floor_plan_id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c40",
"x": 8,
"y": 8,
"w": 12,
"h": 8,
"rotation": 90
},
{
"id": "0199a8f2-6b1c-7c3e-9a51-3f2d8e4b7c45",
"name": "Tub 1",
"kind": "Tub",
"active": true,
"order": 1,
"floor_plan_id": null,
"x": null,
"y": null,
"w": null,
"h": null,
"rotation": null
}
],
"next_cursor": null
}Service records
The service record object
idstring (uuid)required- The record's ID.
status"draft" | "completed" | "voided"requireddraft(being written),completed(kept at least 3 years) orvoided(kept, with a reason).booking_idstring (uuid) or nullrequired- The visit (booking) it records, or null: written for a customer, or the visit's contact was purged.
contact_idstring (uuid) or nullrequired- The customer (see GET /contacts/{id}), or null once the contact is in the trash or purged.
technician_idstring (uuid)required- The technician: a member's ID (see GET /members).
service_atstring (ISO 8601 date-time)required- When the service was done (ISO 8601, UTC).
service_namestringrequired- The service (1–120 characters).
customer_namestringrequired- The customer's name as written on the record.
service_addressstringrequired- The service address as written on the record (empty if none).
technician_namestringrequired- The technician's name, written on the record when it was completed (empty for a draft).
licence_numberstring,nullrequired- The technician's applicator licence in force when the record was completed (null for a draft, or none on file).
conditionsstring,nullrequired- Conditions, like weather and wind (up to 200 characters), or null.
notesstring,nullrequired- Notes (up to 5,000 characters), or null.
completed_atstring (ISO 8601 date-time) or nullrequired- When it was completed (null for a draft).
voided_atstring (ISO 8601 date-time) or nullrequired- When it was voided, or null.
void_reasonstring,nullrequired- Why it was voided, or null.
created_atstring (ISO 8601 date-time)required- When it was created (ISO 8601, UTC).
updated_atstring (ISO 8601 date-time)required- When it last changed (ISO 8601, UTC).
applicationsarray of objectrequired- The products applied, in order (up to 30).
GET/service-records
List service records
Service records not in the trash (voided ones too, with their status), newest first, each with its applications.
Needs: service_records:read
Query parameters
limitintegerdefault 25- Records per page, 1–100 (default 25).
cursorstring- The
next_cursorof the previous page. Use it with the samesort. sort"created_at" | "updated_at"default "created_at"created_at: newest first (default).updated_at: most recently changed first.updated_sincestring (ISO 8601 date-time)- Only records changed at or after this time (ISO 8601, with a time zone).
contact_idstring (uuid)- Only this contact's records.
technician_idstring (uuid)- Only this technician's records (a member's ID).
status"draft" | "completed" | "voided"- Only records with this status.
service_afterstring (ISO 8601 date-time)- Only records serviced at or after this time (ISO 8601, with a time zone).
service_beforestring (ISO 8601 date-time)- Only records serviced before this time (ISO 8601, with a time zone).
Example
curl "https://app.fetchfifi.com/api/v1/service-records" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": [
{
"id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9c01",
"status": "completed",
"booking_id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9c02",
"contact_id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9c03",
"technician_id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9c04",
"service_at": "2026-10-12T14:00:00.000Z",
"service_name": "General pest service",
"customer_name": "Megan Ross",
"service_address": "14 Alder Ln, Raleigh, NC 27609",
"technician_name": "Luis Ortega",
"licence_number": "NC-12345",
"conditions": "Sunny, 72°F, wind 5 mph NW",
"notes": "Perimeter treated; ants at the patio slab.",
"completed_at": "2026-10-12T15:02:00.000Z",
"voided_at": null,
"void_reason": null,
"created_at": "2026-10-12T14:50:00.000Z",
"updated_at": "2026-10-12T15:02:00.000Z",
"applications": [
{
"product_id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9c05",
"product_name": "Bifen I/T",
"epa_reg_no": "53883-118",
"amount": 2.5,
"unit": "fl_oz",
"target_pests": "Ants, spiders",
"areas": "Foundation perimeter, 3 ft band",
"method": "Spray",
"rate": "1 fl oz per gal"
},
{
"product_id": null,
"product_name": "Glue boards",
"epa_reg_no": null,
"amount": 4,
"unit": "each",
"target_pests": "Spiders",
"areas": "Garage corners",
"method": "Trap",
"rate": null
}
]
}
],
"next_cursor": null
}GET/service-records/{id}
Get a service record
A service record not in the trash, with its applications in order.
Needs: service_records:read
Path
idstring (uuid)required- The record's ID (in the path).
Example
curl "https://app.fetchfifi.com/api/v1/service-records/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": {
"id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9c01",
"status": "completed",
"booking_id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9c02",
"contact_id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9c03",
"technician_id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9c04",
"service_at": "2026-10-12T14:00:00.000Z",
"service_name": "General pest service",
"customer_name": "Megan Ross",
"service_address": "14 Alder Ln, Raleigh, NC 27609",
"technician_name": "Luis Ortega",
"licence_number": "NC-12345",
"conditions": "Sunny, 72°F, wind 5 mph NW",
"notes": "Perimeter treated; ants at the patio slab.",
"completed_at": "2026-10-12T15:02:00.000Z",
"voided_at": null,
"void_reason": null,
"created_at": "2026-10-12T14:50:00.000Z",
"updated_at": "2026-10-12T15:02:00.000Z",
"applications": [
{
"product_id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9c05",
"product_name": "Bifen I/T",
"epa_reg_no": "53883-118",
"amount": 2.5,
"unit": "fl_oz",
"target_pests": "Ants, spiders",
"areas": "Foundation perimeter, 3 ft band",
"method": "Spray",
"rate": "1 fl oz per gal"
},
{
"product_id": null,
"product_name": "Glue boards",
"epa_reg_no": null,
"amount": 4,
"unit": "each",
"target_pests": "Spiders",
"areas": "Garage corners",
"method": "Trap",
"rate": null
}
]
}
}GET/service-products
List service products
The workspace's treatment products (at most 500, archived ones included), newest first.
Needs: service_records:read
Query parameters
limitintegerdefault 25- Records per page, 1–100 (default 25).
cursorstring- The
next_cursorof the previous page. Use it with the samesort. sort"created_at" | "updated_at"default "created_at"created_at: newest first (default).updated_at: most recently changed first.updated_sincestring (ISO 8601 date-time)- Only records changed at or after this time (ISO 8601, with a time zone).
Example
curl "https://app.fetchfifi.com/api/v1/service-products" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": [
{
"id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9c05",
"name": "Bifen I/T",
"epa_reg_no": "53883-118",
"active_ingredient": "Bifenthrin 7.9%",
"default_unit": "fl_oz",
"default_method": "Spray",
"archived": false,
"created_at": "2026-09-01T09:00:00.000Z",
"updated_at": "2026-09-01T09:00:00.000Z"
}
],
"next_cursor": null
}Loads
GET/loads
List loads
Loads not in the trash (delivered and cancelled ones too), newest first, each with its stops and its documents' metadata.
Needs: loads:read
Query parameters
limitintegerdefault 25- Records per page, 1–100 (default 25).
cursorstring- The
next_cursorof the previous page. Use it with the samesort. sort"created_at" | "updated_at"default "created_at"created_at: newest first (default).updated_at: most recently changed first.updated_sincestring (ISO 8601 date-time)- Only records changed at or after this time (ISO 8601, with a time zone).
status"open" | "covered" | "dispatched" | "in_transit" | "delivered" | "cancelled"- Only loads with this status.
shipper_idstring (uuid)- Only this shipper's loads (a company's ID).
carrier_idstring (uuid)- Only loads this carrier hauls (a company's ID).
deal_idstring (uuid)- Only loads on this lane (a deal's ID).
pickup_afterstring (ISO 8601 date-time)- Only loads whose first pickup is at or after this time (ISO 8601, with a time zone).
pickup_beforestring (ISO 8601 date-time)- Only loads whose first pickup is before this time (ISO 8601, with a time zone).
Example
curl "https://app.fetchfifi.com/api/v1/loads" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": [
{
"id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9d01",
"number": 48213,
"status": "in_transit",
"owner_id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9d02",
"shipper_id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9d03",
"shipper_contact_id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9d04",
"deal_id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9d05",
"carrier_id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9d06",
"driver_contact_id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9d07",
"truck_number": "214",
"trailer_number": "5531",
"equipment": "dry_van",
"temp_min_f": null,
"temp_max_f": null,
"weight_lb": 38000,
"pieces": 22,
"commodity": "Corrugated boxes",
"hazmat": false,
"reference": "PO 77120",
"rate_cents": 145000,
"carrier_pay_cents": 118000,
"margin_cents": 27000,
"pickup_at": "2026-10-15T13:00:00.000Z",
"delivery_at": "2026-10-16T15:00:00.000Z",
"origin": "Joliet, IL",
"destination": "Memphis, TN",
"delivered_at": null,
"cancelled_at": null,
"cancel_reason": null,
"notes": "Driver to call 30 minutes out.",
"created_at": "2026-10-13T16:20:00.000Z",
"updated_at": "2026-10-15T15:05:00.000Z",
"stops": [
{
"id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9d08",
"position": 0,
"kind": "pickup",
"facility": "Joliet Packaging Co. plant",
"address": "1200 Mound Rd, Joliet, IL 60436",
"locality": "Joliet, IL",
"timezone": "America/Chicago",
"window_start": "2026-10-15T13:00:00.000Z",
"window_end": "2026-10-15T15:00:00.000Z",
"appointment_ref": "JP-5521",
"booking_id": null,
"arrived_at": "2026-10-15T13:10:00.000Z",
"departed_at": "2026-10-15T14:05:00.000Z",
"notes": null
},
{
"id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9d09",
"position": 1,
"kind": "delivery",
"facility": "Delta Distribution Center",
"address": "3450 Lamar Ave, Memphis, TN 38118",
"locality": "Memphis, TN",
"timezone": "America/Chicago",
"window_start": "2026-10-16T13:00:00.000Z",
"window_end": "2026-10-16T15:00:00.000Z",
"appointment_ref": null,
"booking_id": null,
"arrived_at": null,
"departed_at": null,
"notes": "Lumper paid by the receiver."
}
],
"documents": [
{
"id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9d10",
"kind": "rate_confirmation",
"stop_id": null,
"content_type": "application/pdf",
"byte_size": 48213,
"name": "rate-confirmation-48213.pdf",
"created_at": "2026-10-13T16:40:00.000Z"
}
]
}
],
"next_cursor": null
}GET/loads/{id}
Get a load
A load not in the trash, with its stops in order and its documents' metadata (download them from the load's page in Fifi).
Needs: loads:read
Path
idstring (uuid)required- The record's ID (in the path).
Example
curl "https://app.fetchfifi.com/api/v1/loads/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": {
"id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9d01",
"number": 48213,
"status": "in_transit",
"owner_id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9d02",
"shipper_id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9d03",
"shipper_contact_id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9d04",
"deal_id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9d05",
"carrier_id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9d06",
"driver_contact_id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9d07",
"truck_number": "214",
"trailer_number": "5531",
"equipment": "dry_van",
"temp_min_f": null,
"temp_max_f": null,
"weight_lb": 38000,
"pieces": 22,
"commodity": "Corrugated boxes",
"hazmat": false,
"reference": "PO 77120",
"rate_cents": 145000,
"carrier_pay_cents": 118000,
"margin_cents": 27000,
"pickup_at": "2026-10-15T13:00:00.000Z",
"delivery_at": "2026-10-16T15:00:00.000Z",
"origin": "Joliet, IL",
"destination": "Memphis, TN",
"delivered_at": null,
"cancelled_at": null,
"cancel_reason": null,
"notes": "Driver to call 30 minutes out.",
"created_at": "2026-10-13T16:20:00.000Z",
"updated_at": "2026-10-15T15:05:00.000Z",
"stops": [
{
"id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9d08",
"position": 0,
"kind": "pickup",
"facility": "Joliet Packaging Co. plant",
"address": "1200 Mound Rd, Joliet, IL 60436",
"locality": "Joliet, IL",
"timezone": "America/Chicago",
"window_start": "2026-10-15T13:00:00.000Z",
"window_end": "2026-10-15T15:00:00.000Z",
"appointment_ref": "JP-5521",
"booking_id": null,
"arrived_at": "2026-10-15T13:10:00.000Z",
"departed_at": "2026-10-15T14:05:00.000Z",
"notes": null
},
{
"id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9d09",
"position": 1,
"kind": "delivery",
"facility": "Delta Distribution Center",
"address": "3450 Lamar Ave, Memphis, TN 38118",
"locality": "Memphis, TN",
"timezone": "America/Chicago",
"window_start": "2026-10-16T13:00:00.000Z",
"window_end": "2026-10-16T15:00:00.000Z",
"appointment_ref": null,
"booking_id": null,
"arrived_at": null,
"departed_at": null,
"notes": "Lumper paid by the receiver."
}
],
"documents": [
{
"id": "0199b2c4-1d2e-7a3b-8c4d-5e6f7a8b9d10",
"kind": "rate_confirmation",
"stop_id": null,
"content_type": "application/pdf",
"byte_size": 48213,
"name": "rate-confirmation-48213.pdf",
"created_at": "2026-10-13T16:40:00.000Z"
}
]
}
}Invoices
GET/invoices
List invoices
Invoices not in the trash (drafts and void ones too, with their status), newest first, without their lines.
Needs: invoices:read
Query parameters
limitintegerdefault 25- Records per page, 1–100 (default 25).
cursorstring- The
next_cursorof the previous page. Use it with the samesort. sort"created_at" | "updated_at"default "created_at"created_at: newest first (default).updated_at: most recently changed first.updated_sincestring (ISO 8601 date-time)- Only records changed at or after this time (ISO 8601, with a time zone).
status"draft" | "sent" | "partially_paid" | "paid" | "void"- Only invoices with this status.
contact_idstring (uuid)- Only invoices billed to this contact.
company_idstring (uuid)- Only invoices billed to this company.
issued_afterstring (YYYY-MM-DD)- Only invoices issued on or after this date (YYYY-MM-DD).
issued_beforestring (YYYY-MM-DD)- Only invoices issued before this date (YYYY-MM-DD).
overdue"true" | "false"true: only overdue invoices (sent or partially paid, due before today in the workspace owner's time zone).false: only the others.
Example
curl "https://app.fetchfifi.com/api/v1/invoices" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": [
{
"id": "0199c1a2-3b4c-7d5e-8f60-718293a4b5c6",
"number": "INV-1042",
"status": "sent",
"overdue": false,
"contact_id": "0199c1a2-3b4c-7d5e-8f60-718293a4b5c7",
"company_id": "0199c1a2-3b4c-7d5e-8f60-718293a4b5c8",
"bill_to_name": "Harbor Grill",
"bill_to_email": "ap@harborgrill.example",
"bill_to_address": "18 Pier Rd, Wilmington, NC 28401",
"customer_reference": "PO 5521",
"issue_date": "2026-10-15",
"due_date": "2026-11-14",
"terms": "net_30",
"currency": "USD",
"subtotal_cents": 48000,
"tax_label": "Sales tax",
"tax_rate": 7.25,
"tax_cents": 580,
"total_cents": 48580,
"paid_cents": 0,
"balance_cents": 48580,
"notes": "Thank you for your business.",
"issued_at": "2026-10-15T14:02:00.000Z",
"paid_at": null,
"voided_at": null,
"void_reason": null,
"created_at": "2026-10-15T13:40:00.000Z",
"updated_at": "2026-10-15T14:02:00.000Z"
}
],
"next_cursor": null
}GET/invoices/{id}
Get an invoice
An invoice not in the trash, with its lines in order and its payments (newest received first, voided ones included).
Needs: invoices:read
Path
idstring (uuid)required- The record's ID (in the path).
Example
curl "https://app.fetchfifi.com/api/v1/invoices/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": {
"id": "0199c1a2-3b4c-7d5e-8f60-718293a4b5c6",
"number": "INV-1042",
"status": "partially_paid",
"overdue": false,
"contact_id": "0199c1a2-3b4c-7d5e-8f60-718293a4b5c7",
"company_id": "0199c1a2-3b4c-7d5e-8f60-718293a4b5c8",
"bill_to_name": "Harbor Grill",
"bill_to_email": "ap@harborgrill.example",
"bill_to_address": "18 Pier Rd, Wilmington, NC 28401",
"customer_reference": "PO 5521",
"issue_date": "2026-10-15",
"due_date": "2026-11-14",
"terms": "net_30",
"currency": "USD",
"subtotal_cents": 48000,
"tax_label": "Sales tax",
"tax_rate": 7.25,
"tax_cents": 580,
"total_cents": 48580,
"paid_cents": 24000,
"balance_cents": 24580,
"notes": "Thank you for your business.",
"issued_at": "2026-10-15T14:02:00.000Z",
"paid_at": null,
"voided_at": null,
"void_reason": null,
"created_at": "2026-10-15T13:40:00.000Z",
"updated_at": "2026-10-15T14:02:00.000Z",
"lines": [
{
"position": 0,
"description": "General pest service, Oct 3, 2026",
"quantity": 1,
"unit_price_cents": 40000,
"line_total_cents": 40000,
"taxable": false,
"source_type": "booking",
"source_id": "0199c1a2-3b4c-7d5e-8f60-718293a4b5c9"
},
{
"position": 1,
"description": "Bait stations (4)",
"quantity": 4,
"unit_price_cents": 2000,
"line_total_cents": 8000,
"taxable": true,
"source_type": null,
"source_id": null
}
],
"payments": [
{
"id": "0199c1a2-3b4c-7d5e-8f60-718293a4b5d0",
"invoice_id": "0199c1a2-3b4c-7d5e-8f60-718293a4b5c6",
"kind": "payment",
"amount_cents": 24000,
"received_on": "2026-10-20",
"method": "check",
"reference": "Check 1042",
"notes": null,
"voided_at": null,
"void_reason": null,
"processor": null,
"processor_payment_id": null,
"fee_cents": null,
"dispute_status": null,
"created_at": "2026-10-20T16:12:00.000Z",
"updated_at": "2026-10-20T16:12:00.000Z"
}
]
}
}Payments
GET/payments
List payments
Payments and refunds recorded for invoices (money received elsewhere: Fifi doesn't move money), newest first, those of invoices not in the trash. A refund has kind refund and a positive amount_cents. Voided entries (entered in error) are left out unless include_voided=true. Needs the invoices:read scope.
Needs: invoices:read
Query parameters
limitintegerdefault 25- Records per page, 1–100 (default 25).
cursorstring- The
next_cursorof the previous page. Use it with the samesort. sort"created_at" | "updated_at"default "created_at"created_at: newest first (default).updated_at: most recently changed first.updated_sincestring (ISO 8601 date-time)- Only records changed at or after this time (ISO 8601, with a time zone).
invoice_idstring (uuid)- Only payments for this invoice.
received_afterstring (YYYY-MM-DD)- Only payments received on or after this date (YYYY-MM-DD).
received_beforestring (YYYY-MM-DD)- Only payments received before this date (YYYY-MM-DD).
method"cash" | "check" | "bank_transfer" | "card" | "other" | "online"- Only payments by this method.
kind"payment" | "refund"paymentorrefund.include_voided"true" | "false"true: voided entries too (defaultfalse).
Example
curl "https://app.fetchfifi.com/api/v1/payments" \
-H "Authorization: Bearer $FIFI_API_KEY"{
"data": [
{
"id": "0199c1a2-3b4c-7d5e-8f60-718293a4b5d0",
"invoice_id": "0199c1a2-3b4c-7d5e-8f60-718293a4b5c6",
"kind": "payment",
"amount_cents": 24000,
"received_on": "2026-10-20",
"method": "check",
"reference": "Check 1042",
"notes": null,
"voided_at": null,
"void_reason": null,
"processor": null,
"processor_payment_id": null,
"fee_cents": null,
"dispute_status": null,
"created_at": "2026-10-20T16:12:00.000Z",
"updated_at": "2026-10-20T16:12:00.000Z"
}
],
"next_cursor": null
}