API keys and the REST API
Use the Fifi API to connect other tools to your workspace: read and write contacts, companies, deals, tasks and timeline entries, manage webhooks, and read your floor plans and seats and your service records.
The full reference, with every field and an example for every endpoint, is the public /developers page in Fifi (also linked from Settings → API). The machine-readable OpenAPI 3.1 document is at /api/v1/openapi.json. This guide covers the essentials.
Who can do it: owners and admins create and revoke keys. Members don't see Settings → API.
Plan: the API is part of the Pro and Business plans. During the beta every workspace is on Business. On Free, every request gets 403 module_disabled. The API module can also be turned off in Settings → Modules.
Create a key
- Go to Settings → API.
- Click Create API key.
- Enter a Name so you know which integration uses it, for example "Zapier".
- Under Access, choose for each area: No access, Read or Read and write. The areas are Contacts, Companies, Deals, Tasks, "Notes, calls and meetings", Webhooks, "Floor plans and seats", "Service records", "Loads" and "Invoices and payments". The last four offer only No access or Read: the API can't change them. Give the key only what the integration needs.
- Choose when it Expires: In 30 days, In 90 days (the default), In a year, or Never.
- Click Create key.
- Copy the key from Copy your API key and store it somewhere safe, like a password manager. You won't see it again.
A key looks like fifi_k3x9q2ab_ followed by 43 letters and digits.
What you'll see: the key appears in the list with its status (Active, Paused, Expired or Revoked), its prefix, its access, who created it, when it expires, and when and from where it was last used. The counter shows how many of your 10 active keys you're using.
What a key can do
- It only ever reaches your workspace.
- It acts as you, the person who created it, limited to the access you gave it. The activity log shows its changes as "‹you› via API key “‹name›”".
- It stops working when it's revoked or expires, while the workspace is suspended or being deleted, or if you're deactivated, removed or no longer an owner or admin. In those last cases Fifi revokes it for good.
Revoke a key
- In Settings → API, find the key and click Revoke.
- Confirm with Revoke key.
Anything using it stops working straight away. This can't be undone; create a new key instead.
Make a request
Send the key in the Authorization header:
export FIFI=https://app.fetchfifi.com # your Fifi address
export FIFI_API_KEY=fifi_xxxxxxxx_... # from Settings → API
curl "$FIFI/api/v1/me" \
-H "Authorization: Bearer $FIFI_API_KEY"/me tells you the workspace, the key's name, prefix, scopes and expiry, and the rate limits.
Rules for every request:
- Never send a Fifi cookie with a key. A request with both gets
400 cookie_not_allowed. - Bodies are JSON with
Content-Type: application/json, at most 256 KB. - Unknown fields are refused (
400). - Times are ISO 8601 in UTC. Dates are
YYYY-MM-DD. Money is whole cents (value_cents).
List contacts
curl "$FIFI/api/v1/contacts?limit=50&lead_status=qualified" \
-H "Authorization: Bearer $FIFI_API_KEY"Lists return { "data": [...], "next_cursor": "..." }. To get the next page, pass cursor=<next_cursor>. When next_cursor is null you have everything.
limit: 1 to 100, default 25.sort:created_at(default) orupdated_at, newest first.updated_since: an ISO time with a time zone, for syncing changes.- Deleted records are never listed.
Create a contact
curl -X POST "$FIFI/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@example.com","lead_status":"new","source":"inbound"}'Fifi answers 201 with { "data": { ... } }. The owner defaults to the key's creator unless you send owner_id.
Update and delete
# Change only the fields you send; null clears an optional field.
curl -X PATCH "$FIFI/api/v1/contacts/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"CTO"}'
# Contacts, companies and deals go to Trash (restorable for 30 days in Fifi).
curl -X DELETE "$FIFI/api/v1/contacts/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY"Deleting a task or an activity is permanent, as it is in the app.
Concurrent changes. A PATCH changes only the fields you send, and the last write wins. To be sure you don't overwrite a change made since you read a contact, company or deal (in Fifi or by another integration), send the updated_at you read as expected_updated_at, exactly as Fifi returned it. If the record has changed since, nothing changes and Fifi answers 409 conflict with details.reason "stale": fetch it again and retry. A value that isn't a time Fifi could have returned (before 1970, or past the year 9999) is a 400. Every change moves updated_at on, including a PATCH that sends the values the record already has.
curl -X PATCH "$FIFI/api/v1/contacts/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"CTO","expected_updated_at":"2026-10-02T09:30:00.000Z"}'Close a deal
curl -X PATCH "$FIFI/api/v1/deals/<id>" \
-H "Authorization: Bearer $FIFI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status":"lost","lost_reason":"timing"}'lost_reason is one of price, timing, competitor, no_decision or other. With other, also send lost_reason_note. Send "status":"open" to reopen.
Endpoints
All paths start with /api/v1.
| Endpoint | Scope needed |
|---|---|
GET /me | any key |
GET /members (id, name, role; never email addresses) | the read scope of contacts, companies, deals, tasks, activities or webhooks (floor_plans:read, service_records:read, loads:read or invoices:read alone isn't enough) |
GET /tags | contacts:read |
GET /pipeline (stages, statuses, lost reasons) | deals:read |
GET, POST /contacts; GET, PATCH, DELETE /contacts/{id} | contacts:read / contacts:write |
GET, POST /companies; GET, PATCH, DELETE /companies/{id} | companies:read / companies:write |
GET, POST /deals; GET, PATCH, DELETE /deals/{id} | deals:read / deals:write |
GET, POST /tasks; GET, PATCH, DELETE /tasks/{id} | tasks:read / tasks:write |
GET, POST /activities; GET, PATCH, DELETE /activities/{id} | activities:read / activities:write |
GET, POST /webhooks; GET, DELETE /webhooks/{id} | webhooks:read / webhooks:write |
GET /floor-plans; GET /floor-plans/{id}; GET /seats | floor_plans:read (read only) |
GET /service-records; GET /service-records/{id}; GET /service-products | service_records:read (read only) |
GET /loads; GET /loads/{id} | loads:read (read only) |
GET /invoices; GET /invoices/{id}; GET /payments | invoices:read (read only) |
GET /openapi.json | none (public) |
Filters by list:
| List | Filters |
|---|---|
| Contacts | owner_id, lead_status, tag_id, company_id, email |
| Companies | owner_id, domain |
| Deals | owner_id, stage, status, company_id, contact_id |
| Tasks | assignee_id, completed, due_from, due_to, record_type with record_id |
| Activities | record_type with record_id, type, author_id |
Activities are notes, calls and meetings (type: note, call, meeting). Only the author can edit one.
If the Deals module is off, deal endpoints and /pipeline answer 403 module_disabled.
Floor plans and seats
Read your floor plans, what's on them, and your appointment seats (tables, tubs, chairs) with a key that has Floor plans and seats: Read. There's no way to change them through the API yet.
curl "$FIFI/api/v1/floor-plans" -H "Authorization: Bearer $FIFI_API_KEY"
curl "$FIFI/api/v1/floor-plans/<id>" -H "Authorization: Bearer $FIFI_API_KEY"
curl "$FIFI/api/v1/seats" -H "Authorization: Bearer $FIFI_API_KEY"- A floor plan:
id,name,columnsandrows(its grid, in cells),version,has_background,created_at,updated_at.versionstarts 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. - One plan (
/floor-plans/{id}) adds itsshapes(walls, doors, areas, tables and labels) and itspods: each kennel ("kind": "kennel") and seat ("kind": "seat") placed on it, with itsidandname. - A seat:
id,name,kind(Table, Tub…),active(false when switched off),order, and where it's placed:floor_plan_idand its position, allnullwhen it isn't on a plan. - Positions and sizes (
x,y,w,h) are in quarter cells (4 to a grid cell):xandyare the top-left corner,wandhthe size before turning, androtationis 0, 90, 180 or 270 degrees. - Kennels show on plans only while Boarding is on, and seats only while Online booking is on (
/seatsis empty while it's off). With both off, all three answer403 module_disabled. - The background image itself isn't available through the API.
Service records
Read your service records (what was applied on each visit) and your product list with a key that has Service records: Read. There's no way to change them through the API, and no service_records:write scope.
curl "$FIFI/api/v1/service-records?status=completed&service_after=2026-10-01T00:00:00Z" -H "Authorization: Bearer $FIFI_API_KEY"
curl "$FIFI/api/v1/service-records/<id>" -H "Authorization: Bearer $FIFI_API_KEY"
curl "$FIFI/api/v1/service-products" -H "Authorization: Bearer $FIFI_API_KEY"- A record:
id,status(draft,completed,voided),booking_idandcontact_id(null once deleted for good, or when written for a customer),technician_id,service_at,service_name,customer_name,service_address,technician_nameandlicence_number(as written when completed),conditions,notes,completed_at,voided_at,void_reason,created_at,updated_at, and itsapplications:product_id,product_name,epa_reg_no,amount(a number with up to 3 decimals),unit,target_pests,areas,method,rate. - Filters:
contact_id,technician_id,status,service_after,service_before, plus the usual paging. Records in the Trash aren't listed. - While Service records is off, all three answer
403 module_disabled(nothing is deleted).
Loads
Read your loads (with their stops and their documents' details) with a key that has Loads: Read. There's no way to change them through the API yet, and no loads:write scope.
curl "$FIFI/api/v1/loads?status=in_transit&pickup_after=2026-10-01T00:00:00Z" -H "Authorization: Bearer $FIFI_API_KEY"
curl "$FIFI/api/v1/loads/<id>" -H "Authorization: Bearer $FIFI_API_KEY"- A load:
id,number,status(open,covered,dispatched,in_transit,delivered,cancelled),owner_id,shipper_id,shipper_contact_id,deal_id(its lane),carrier_id,driver_contact_id,truck_number,trailer_number,equipment,temp_min_f,temp_max_f,weight_lb,pieces,commodity,hazmat,reference, the money in cents (rate_cents,carrier_pay_cents, andmargin_cents, worked out),pickup_at,delivery_at,origin,destination,delivered_at,cancelled_at,cancel_reason,notes,created_at,updated_at; - its
stopsin order:id,position,kind(pickupordelivery),facility,address,locality,timezone(each stop's own),window_start,window_end,appointment_ref,booking_id(a dock appointment),arrived_at,departed_at,notes; - its
documents:id,kind(pod,rate_confirmation,bol,other),stop_id,content_type,byte_size,name,created_at. The files themselves aren't available through the API: download them from the load's page. - Filters:
status,shipper_id,carrier_id,deal_id,pickup_after,pickup_before, plus the usual paging andupdated_since(for "changed since"). Loads in the Trash aren't listed. - While Loads is off, both answer
403 module_disabled(nothing is deleted).
Invoices
Read your invoices and their payments with a key that has Invoices and payments: Read (invoices:read). There's no way to create or change them through the API yet, and no invoices:write scope.
curl "$FIFI/api/v1/invoices?status=sent&overdue=true" -H "Authorization: Bearer $FIFI_API_KEY"
curl "$FIFI/api/v1/invoices/<id>" -H "Authorization: Bearer $FIFI_API_KEY"
curl "$FIFI/api/v1/payments?invoice_id=<id>" -H "Authorization: Bearer $FIFI_API_KEY"- An invoice:
id,number(null for a draft),status(draft,sent,partially_paid,paid,void),overdue(worked out: sent or partially paid and past its due date in the workspace owner's time zone),contact_id,company_id(null once those are deleted: the invoice keeps its own copy of whom it's billed to),bill_to_name,bill_to_email,bill_to_address,customer_reference,issue_date,due_date,terms,currency(USD), the money in cents (subtotal_cents,tax_cents,total_cents,paid_cents,balance_cents: the total less what is paid, and 0 for a void invoice),tax_label,tax_rate(a percent with up to 3 decimals),notes,issued_at,paid_at,voided_at,void_reason,created_at,updated_at; - on
GET /invoices/{id}, itslinesin order:position,description,quantity(up to 3 decimals),unit_price_cents,line_total_cents,taxable, and the record it bills (source_type:booking,stay,service_record,dealorload, andsource_id). - Filters:
status,contact_id,company_id,issued_after,issued_before(YYYY-MM-DD),overdue(trueorfalse), plus the usual paging andupdated_since. Invoices in the Trash aren't listed. - A payment (
GET /payments, andpaymentsonGET /invoices/{id}):id,invoice_id,kind(paymentorrefund;amount_centsis always positive),amount_cents,received_on(YYYY-MM-DD),method(cash,check,bank_transfer,cardtaken outside Fifi,other),reference,notes,voided_at,void_reason(a voided payment was entered in error and counts for nothing),created_at,updated_at. Filters:invoice_id,received_after(on or after),received_before(before),method,kind,include_voided(true: voided entries too; left out by default), plus the usual paging. - While Invoicing is off, all three answer
403 module_disabled(nothing is deleted).
Retrying safely: Idempotency-Key
Add an Idempotency-Key header (any printable text up to 255 characters, such as a UUID) to a POST. If you send the same request again with the same key within 24 hours, Fifi returns the first response with Idempotent-Replayed: true instead of creating a duplicate.
- Same key, different body:
422 idempotency_key_reused. - Same key, while the first is still running:
409 idempotency_key_in_use. Retry a moment later.
Rate limits
- 120 requests a minute per key and 600 per workspace (on Pro, the workspace limit is 300).
- Each response carries
RateLimit-Limit,RateLimit-Remaining,RateLimit-ResetandRateLimit-Policy. - Over the limit you get
429with aRetry-Afterheader (seconds). - Too many bad keys from one address (60 a minute) are blocked for a while, even with a good key.
Errors
Errors look like this:
{ "error": { "code": "invalid_request", "message": "Some fields aren't valid.", "details": { "fields": { "email": "..." } } } }| Status | Common codes | What to do |
|---|---|---|
| 400 | invalid_request, invalid_json, invalid_cursor, cookie_not_allowed | Fix the request; details.fields says which field |
| 401 | unauthorized | Check the key; it may be revoked or expired |
| 403 | insufficient_scope, forbidden, module_disabled, sample_workspace | Give the key the scope in details.required, or turn the module on. sample_workspace: the key belongs to a sample workspace, where the API never works |
| 404 | not_found | The record doesn't exist, is deleted, or belongs to another workspace |
| 409 | conflict (duplicate_email, duplicate_domain, stale), idempotency_key_in_use | Use the existing record (details.existing_id) or retry; stale: the record changed after the expected_updated_at you sent, so fetch it again first |
| 413, 415 | payload_too_large, unsupported_media_type | Keep bodies under 256 KB, send JSON |
| 422 | rule_violation, idempotency_key_reused | A business rule refused it; read the message |
| 429 | rate_limited | Wait for Retry-After |
| 500 | internal | Retry; quote the X-Request-Id header if you contact support |
| 503 | api_disabled | The API isn't available on this Fifi |
Every response has an X-Request-Id header. Fifi never sends CORS headers, so call the API from a server, not a browser.
Troubleshooting
- 401 straight after creating a key: make sure you copied the whole key, including
fifi_. - A key shows "Paused": its creator's account isn't active. Create a new key as an active owner or admin.
- 403 insufficient_scope: edit isn't possible; create a new key with the right access and revoke the old one.
- Changes don't show in a sync by
updated_since: pass the time with a time zone, for example2026-10-05T00:00:00Z.