StoreFleet API

Read a store's customers, orders, products, requests and texts. File requests, approve price suggestions and text customers under the store's own rules. Get a webhook when something happens. Plain JSON over HTTPS, one key per app.

Getting started

  1. StoreFleet switches the API on for your store. Ask us at jake@storefleet.com.
  2. The store owner opens Settings > API in StoreFleet, creates a key, names it after the app that will use it, and ticks the permissions it needs.
  3. Copy the key when it appears. It is shown once.
  4. Make your first call. It answers with the store and what the key may do.
curl https://storefleet.com/api/v1/me \
  -H "Authorization: Bearer sf_live_your_key"
{
  "store": {
    "id": "8d3f0c3e-6c55-4d9b-9b0e-6a3f2a1b7c10",
    "name": "Luxury Box",
    "slug": "luxurybox",
    "timezone": "America/New_York"
  },
  "credential": {
    "id": "1b7e5d2a-0f43-4a3c-9a3b-2b1c9c4e8f01",
    "name": "Store dashboard",
    "kind": "key",
    "mode": "live"
  },
  "scopes": [
    "read:orders",
    "read:metrics"
  ]
}

Every address in this guide starts with https://storefleet.com/api/v1. The full list is in the endpoint reference and the OpenAPI file.

Authentication

Send the key in the Authorization header of every request: Authorization: Bearer sf_live_.... Never put a key in a URL. A key belongs to one store, so you never pass a store id: the key decides which store you see.

KeyWhat it does
sf_live_...Reads and writes for real. Texts go out.
sf_test_...Reads real data. Every write and every text is a dry run: StoreFleet checks the request the way the real call would, changes nothing and sends nothing.

A test key answers a write with 200 and { "dry_run": true, "operation", "result" }, where result shows what the call would have done. Build with a test key, then switch to a live one.

  • Keep a key like a password. Anyone holding it can do what its permissions allow.
  • The owner can revoke a key in Settings > API. It stops working on the very next call.
  • A missing or wrong key answers 401 unauthorized. If the API is not switched on for the store, calls answer 403 api_disabled.
  • An app that many stores connect to uses OAuth instead of keys. See Connect an app. Its access tokens go in the same header.

Permissions

A key or app holds a list of permissions. Each endpoint needs one. A call without it answers 403 insufficient_scope with required_scope naming the one it needed.

PermissionAllowsNotes
read:contactsRead customers
read:contact_infoRead customer phone numbers and emailsOff unless the owner ticks it
write:contactsEdit customersIncludes read:contacts
read:ordersRead orders
read:productsRead products
read:inquiriesRead requests
write:inquiriesCreate and update requestsIncludes read:inquiries
read:messagesRead texts
send:messagesText customersOff unless the owner ticks it
read:repricesRead price suggestions
write:repricesApprove price suggestionsIncludes read:reprices
read:metricsRead store numbers
manage:webhooksManage webhooks
read:teamRead team members
write:contact_ownerAssign customers to team membersIncludes read:team

Two permissions are never given by default. The owner ticks each one by name for each key or app: read:contact_info, which lets customer phone numbers and emails leave StoreFleet, and send:messages, which lets your program text customers from the store's number.

Requests and responses

  • HTTPS only. JSON in, JSON out.
  • GET and DELETE take query parameters. POST, PUT and PATCH take a JSON body with Content-Type: application/json, up to 64 KB.
  • Ids are StoreFleet ids. An id from another store answers 404, the same as one that does not exist.
  • Every object carries an object field naming its kind, such as contact, order or message.
  • Times are ISO 8601 strings.
  • Customer phone numbers and emails are null unless the key holds read:contact_info.
  • Every response carries an X-Request-Id header. Quote it when you ask us about a call.

Paging

List endpoints answer newest first, one page at a time: { "data": [...], "has_more": true, "next_cursor": "..." }. Ask for up to limit rows (50 by default, 200 at most). For the next page, send cursor set to the next_cursor you got. When has_more is false, next_cursor is null and you have every row.

Treat a cursor as opaque and pass it back as it came. Rows added between your calls never repeat or go missing.

let cursor = null;
do {
  const url = new URL("https://storefleet.com/api/v1/contacts");
  url.searchParams.set("limit", "200");
  if (cursor) url.searchParams.set("cursor", cursor);

  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.STOREFLEET_KEY}` },
  });
  const page = await res.json();
  for (const customer of page.data) {
    // use the customer
  }
  cursor = page.has_more ? page.next_cursor : null;
} while (cursor);

Errors

A failure answers with an HTTP status and one shape. Branch on code; the message is for people and may change.

{
  "error": {
    "code": "insufficient_scope",
    "message": "this call needs the read:orders permission",
    "request_id": "req_Xz3k9QmW2pLr7TbV",
    "required_scope": "read:orders"
  }
}
StatusCodeMeaning
400invalid_requestThe request is not valid: a missing or malformed field, a bad cursor, or a body that is not JSON. issues lists each problem.
401unauthorizedNo key or token was sent, or it is wrong, revoked or expired.
403insufficient_scopeThe key or app lacks the permission this call needs. required_scope names it.
403forbiddenThe call is not allowed for this store, for example a feature the store has switched off.
403store_inactiveThe store is switched off.
403api_disabledThe API is not switched on for this store.
404not_foundNo such row in this store.
409conflictThe row is not in a state that allows this, or a request with the same Idempotency-Key is still running.
409idempotency_conflictThis Idempotency-Key was already used for a different request.
422send_refusedThe store's texting rules refused the text. reason says which rule.
422refusedThe store's rules refused the change. The message says why.
429rate_limitedToo many requests. Wait the number of seconds in Retry-After.
500server_errorSomething broke on our side. The request_id helps us find it.
503unavailableA part of StoreFleet is briefly unavailable. Try again shortly.

Retry a 429 after Retry-After seconds, and a 500 or 503 after a growing wait. Other errors will answer the same way again until the request changes.

Rate limits

LimitCounted per
120 requests a minutekey or app
30 writes a minute, inside thatkey or app
20,000 requests a daystore, shared by every key and app

Every response reports the per-minute window in RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds until it resets). Past a limit, calls answer 429 rate_limited with a Retry-After header in seconds.

Idempotency

A write that times out leaves you unsure whether it happened. Send an Idempotency-Key header with any unique string for that action (1 to 255 letters, digits, or - _ : .). If you retry with the same key and the same request, StoreFleet answers with the first outcome instead of doing it again.

  • The same key while the first request is still running answers 409 conflict. Try again shortly.
  • The same key with a different request answers 409 idempotency_conflict.
  • A server error (5xx) is not remembered, so a retry runs again.
  • Keys are remembered for at least 48 hours.
  • Required on: POST /messages. Accepted on every other write except these, which are never replayed: POST /webhook-endpoints, POST /webhook-endpoints/{id}/rotate-secret, POST /webhook-endpoints/{id}/test.

Texting customers

POST /api/v1/messages

Needs send:messages, which is off unless the owner ticks it, and an Idempotency-Key header. The text goes through the same checks as every text the store sends. Nothing about the API skips them:

  • Customers who opted out, or who are kept out of automatic texts, are never texted.
  • Quiet hours: texts go out only between 9am and 9pm, store time.
  • The store's daily limit. It resets at midnight, store time.
  • No repeating the same words to one customer within 24 hours.
  • The customer needs a mobile number on file.
curl -X POST https://storefleet.com/api/v1/messages \
  -H "Authorization: Bearer sf_live_your_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: restock-4471-jordan-4" \
  -d '{
    "contact_id": "5b1e7c2a-3d4f-4a6b-8c9d-0e1f2a3b4c5d",
    "body": "Hi Sam, the Jordan 4 in size 10 you asked about just came in."
  }'
AnswerWhat it means
201Sent. The body is the text, with its status.
202status: "unknown". The text may or may not have gone out. Do not retry it, with this key or a new one. Staff see it in StoreFleet and settle it there.
422The rules said no. send_refused with a reason.
{
  "error": {
    "code": "send_refused",
    "message": "it is outside the store's texting hours (9am to 9pm store time)",
    "request_id": "req_Lq8vN2cR5sYw1HdK",
    "reason": "quiet_hours"
  }
}
ReasonWhy
opted_outThe customer opted out, or is kept out of automatic texts.
unreachableThe customer's number is marked unreachable.
no_phoneNo mobile number on file.
quiet_hoursOutside 9am to 9pm, store time.
daily_limitThe store reached its daily limit.
duplicateThe same text went to this customer recently.
not_allowedThe store can only text its test numbers right now.
pausedTexting is paused for this store.
store_not_readyThe store is not set up to text yet.
own_numberThat number is one of the store's own lines.
no_lineNo sending line can carry this text right now.
refusedAnother of the store's rules said no.

A refusal is an answer, not a fault. Do not retry around it. With a test key the call checks the customer and their number and sends nothing.

Webhooks

StoreFleet can post an event to your server when something happens. Add an endpoint in Settings > API, or with POST /webhook-endpoints (permission manage:webhooks). Its signing secret, whsec_..., is shown once. Events arrive within about a minute.

EventWhenPermission
inquiry.createdA customer request was filedread:inquiries
message.sentThe store sent a textread:messages
message.receivedA customer texted the storeread:messages
order.paidAn order was paidread:orders
price_suggestion.createdA new price suggestion is waitingread:reprices
contact.createdA new customer was addedread:contacts
pingA test event you ask for from Settings or the test endpointnone

An endpoint only receives events its creator's permissions can read. Each event names the row it is about, and data.object has the same shape the matching endpoint answers. If the row is gone, data.object is null and data.deleted is true. Phone numbers and emails appear only when the endpoint was set to include them, which needs read:contact_info.

{
  "id": "evt_5f0c2b8e9d4a4c1b8a7e6d5c4b3a2f10",
  "object": "event",
  "type": "inquiry.created",
  "created_at": "2026-10-04T15:02:11.000Z",
  "store_id": "8d3f0c3e-6c55-4d9b-9b0e-6a3f2a1b7c10",
  "data": {
    "object": {
      "id": "2c9a4e1b-7f3d-4b5a-9e8c-1d2f3a4b5c6d",
      "object": "inquiry",
      "contact_id": "5b1e7c2a-3d4f-4a6b-8c9d-0e1f2a3b4c5d",
      "first_name": "Sam",
      "last_name": null,
      "phone": null,
      "email": null,
      "product": "Jordan 4 Bred",
      "size": "10",
      "status": "open",
      "kind": "source",
      "source": "form",
      "match_preference": "similar",
      "matched_product": null,
      "matched_at": null,
      "notified_at": null,
      "closed_at": null,
      "close_reason": null,
      "converted": false,
      "converted_order_id": null,
      "submitted_at": "2026-10-04T15:01:58.000Z",
      "created_at": "2026-10-04T15:01:58.000Z"
    }
  }
}

Check the signature

Every delivery carries three headers, in the Standard Webhooks format: webhook-id (the event id), webhook-timestamp (unix seconds) and webhook-signature (v1, then a base64 HMAC-SHA256 of id.timestamp.body). The HMAC key is the part of your secret after whsec_, base64-decoded. Refuse any delivery more than five minutes old. Any Standard Webhooks library will do this for you; here it is in plain Node:

import crypto from "node:crypto";

// secret:  the endpoint's signing secret, whsec_...
// headers: the request's headers, with lowercase names
// rawBody: the body exactly as it arrived, before any JSON parsing
export function verifyStoreFleetWebhook(secret, headers, rawBody) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatures = headers["webhook-signature"];
  if (!id || !timestamp || !signatures) return false;

  // Refuse a delivery more than five minutes from now.
  const ts = Number(timestamp);
  const now = Math.floor(Date.now() / 1000);
  if (!Number.isInteger(ts) || Math.abs(now - ts) > 300) return false;

  // The key is the part after whsec_, base64-decoded.
  const key = Buffer.from(secret.slice("whsec_".length), "base64");
  const mac = crypto
    .createHmac("sha256", key)
    .update(`${id}.${ts}.${rawBody}`, "utf8")
    .digest("base64");
  const expected = Buffer.from(`v1,${mac}`);

  // The header may carry several signatures, separated by spaces.
  return signatures.split(" ").some((candidate) => {
    const got = Buffer.from(candidate);
    return got.length === expected.length && crypto.timingSafeEqual(got, expected);
  });
}

Answering and retries

  • Answer with any 2xx within 10 seconds. Redirects are not followed.
  • Anything else is a failure. The event is tried again after waits of 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours, 1 day, 1 day: 9 tries in all, over about three days.
  • The same event can arrive twice. Use webhook-id to skip one you already handled.
  • A retried event can arrive after a newer one. Order by created_at.
  • An endpoint that fails every delivery for 3 days is switched off. The owner turns it back on in Settings, or you send PATCH /webhook-endpoints/{id} with active set to true.

Connect an app

If you build an app that many stores use, have each owner connect it instead of pasting a key. This is standard OAuth 2.1 with PKCE. The owner signs in to StoreFleet, sees your app's name and the permissions it asks for, and approves. The two sensitive permissions stay off unless the owner ticks them. The owner can disconnect your app at any time in Settings > API.

StepAddress
Discoveryhttps://storefleet.com/.well-known/oauth-authorization-server
Send the owner tohttps://storefleet.com/oauth/authorize
Swap the code for tokenshttps://storefleet.com/api/oauth/token
Register your apphttps://storefleet.com/api/oauth/register
Revoke a tokenhttps://storefleet.com/api/oauth/revoke
  • Your client id can be the https address of a small JSON file you host describing your app (a client ID metadata document), or one you get by registering at the address above.
  • PKCE with S256 is required. Redirect addresses must be https, or http on your own machine for local tools.
  • Ask for permissions in scope, separated by spaces, using the names in the permissions table.
  • Access tokens last 1 hour. Refresh tokens last 60 days and change every time you use one: keep the new one, the old one stops working.
  • Send the access token as Authorization: Bearer ..., the same as a key.

AI assistants

StoreFleet runs an MCP server, so an AI assistant can read the store and act for the owner with the same permissions and rules as the API.

Server address: https://storefleet.com/api/mcp

  • Assistants with a Connect button: add the address as a custom connector. The assistant sends the owner to StoreFleet to approve it, the same as connecting an app.
  • Assistants that take a header: add the address and send a key as Authorization: Bearer .... Start with a test key, so anything the assistant writes or texts is a dry run.
{
  "mcpServers": {
    "storefleet": {
      "url": "https://storefleet.com/api/mcp",
      "headers": { "Authorization": "Bearer sf_test_your_key" }
    }
  }
}
ToolWhat it doesPermission
whoamiWhich store am I connected tonone
list_customersList customersread:contacts
get_customerGet one customerread:contacts
set_customer_manual_tagsReplace a customer's manual tagswrite:contacts
list_customer_notesList a customer's notesread:contacts
add_customer_noteAdd a note to a customerwrite:contacts
set_customer_ownerSet which team member owns a customerwrite:contact_owner
set_customer_ownersSet the owner of many customers at oncewrite:contact_owner
list_teamList the store's team membersread:team
list_ordersList ordersread:orders
get_orderGet one order with its itemsread:orders
list_productsList productsread:products
get_productGet one product with its sizesread:products
list_requestsList customer requestsread:inquiries
get_requestGet one customer requestread:inquiries
create_requestFile a customer requestwrite:inquiries
close_requestClose a customer requestwrite:inquiries
reopen_requestReopen a closed customer requestwrite:inquiries
list_textsList texts sent or receivedread:messages
get_textGet one sent textread:messages
send_textText a customer from the store's numbersend:messages
list_price_suggestionsList price suggestionsread:reprices
get_price_suggestionGet one price suggestionread:reprices
approve_price_suggestionApprove a price suggestion (changes the live price)write:reprices
skip_price_suggestionSkip a price suggestionwrite:reprices
store_numbersStore numbers for a date rangeread:metrics

Endpoint reference

Every endpoint, generated from the same list the API runs on. The same reference as a machine-readable file: /api/v1/openapi.json.

Account

GET /api/v1/me

Which store this credential belongs to and what it may do

Answers with the store, the credential's name, whether it is a key or a connected app, its permissions, and whether it is in test mode. Any valid credential may call it.

Permission: any valid key or app. Success: 200.

No parameters.

{
  "store": {
    "id": "8d3f0c3e-6c55-4d9b-9b0e-6a3f2a1b7c10",
    "name": "Luxury Box",
    "slug": "luxurybox",
    "timezone": "America/New_York"
  },
  "credential": {
    "id": "1b7e5d2a-0f43-4a3c-9a3b-2b1c9c4e8f01",
    "name": "Store dashboard",
    "kind": "key",
    "mode": "live"
  },
  "scopes": [
    "read:orders",
    "read:metrics"
  ]
}

Customers

GET /api/v1/contacts

List customers, most recently updated first

Phone numbers and emails are null unless the credential holds read:contact_info. Filter by tag, manual tag, tier, owner, or updated_since to sync only what changed.

Permission: read:contacts. Success: 200.

FieldInTypeRequiredNotes
limitqueryintegerRows per page, 1 to 200. Default 50.
cursorquerystringThe next_cursor from the previous page. Up to 512 characters.
updated_sincequerystringOnly customers changed at or after this time, ISO 8601 (2026-10-01 or 2026-10-01T09:00:00Z).
tagquerystringOnly customers carrying this tag. Up to 100 characters.
manual_tagquerystringOnly customers carrying this manual tag. Up to 40 characters.
tierquerystringOnly customers in this VIP tier. Up to 40 characters.
ownerquery"none" or idOnly customers this team member owns (a team member id from GET /team), or none for customers nobody owns.

GET /api/v1/contacts/{id}

One customer

Permission: read:contacts. Success: 200.

FieldInTypeRequiredNotes
idpathidyesThe StoreFleet id.

PUT /api/v1/contacts/{id}/manual-tags

Replace a customer's manual tags

Up to 20 tags of up to 40 characters each. The list you send replaces the current one; send [] to clear.

Permission: write:contacts. Success: 200. Idempotency-Key: accepted.

FieldInTypeRequiredNotes
idpathidyesThe StoreFleet id.
manual_tagsbodylist of stringyesThe full list of manual tags to keep. Up to 20 items.

GET /api/v1/contacts/{id}/notes

A customer's notes, newest first

Permission: read:contacts. Success: 200.

FieldInTypeRequiredNotes
idpathidyesThe StoreFleet id.
limitqueryintegerRows per page, 1 to 200. Default 50.
cursorquerystringThe next_cursor from the previous page. Up to 512 characters.

POST /api/v1/contacts/{id}/notes

Add a note to a customer

Plain text, up to 2000 characters. The note shows on the customer's card in StoreFleet with the app's name as its author.

Permission: write:contacts. Success: 201. Idempotency-Key: accepted.

FieldInTypeRequiredNotes
idpathidyesThe StoreFleet id.
bodybodystringyesThe note. Up to 2000 characters.

PUT /api/v1/contacts/{id}/owner

Set which team member owns a customer

Only active team members can own a customer. The change shows on the customer's card in StoreFleet as a note with the app's name.

Permission: write:contact_owner. Success: 200. Idempotency-Key: accepted.

FieldInTypeRequiredNotes
idpathidyesThe StoreFleet id.
team_member_idbodyid or anyyesThe team member who should own the customer (an id from GET /team), or null for nobody.

POST /api/v1/contacts/owners

Set the owner of many customers at once

Up to 100 customers per call. Each one answers on its own: a customer that is not found, or a team member who is not active, does not stop the rest.

Permission: write:contact_owner. Success: 200. Idempotency-Key: accepted.

FieldInTypeRequiredNotes
assignmentsbodylist of objectyesWhich customer goes to which team member. Up to 100 items.

Team

GET /api/v1/team

List the store's team members

Every team member, newest first. Only active ones can own a customer; set active=true to list just them.

Permission: read:team. Success: 200.

FieldInTypeRequiredNotes
limitqueryintegerRows per page, 1 to 200. Default 50.
cursorquerystringThe next_cursor from the previous page. Up to 512 characters.
activequerystringtrue for current team members only, false for former ones only. One of: true, false.
{
  "data": [
    {
      "id": "5a1f7c2e-3b8d-4e6a-9c0f-1d2e3f4a5b6c",
      "object": "team_member",
      "name": "Leah",
      "role": "Sales associate",
      "location": "buckhead",
      "active": true,
      "former_at": null,
      "created_at": "2026-08-01T15:00:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Orders

GET /api/v1/orders

List orders, newest recorded first

An order appears here when it is paid. Filter by when the customer bought (since, until) or by one customer. The list runs newest recorded first, so an imported older order can sit near the top; each order's ordered_at says when it was placed.

Permission: read:orders. Success: 200.

FieldInTypeRequiredNotes
limitqueryintegerRows per page, 1 to 200. Default 50.
cursorquerystringThe next_cursor from the previous page. Up to 512 characters.
sincequerystringOnly orders placed at or after this time, ISO 8601 (2026-10-01 or 2026-10-01T09:00:00Z).
untilquerystringOnly orders placed before this time, ISO 8601 (2026-10-01 or 2026-10-01T09:00:00Z).
contact_idqueryidOnly this customer's orders.

GET /api/v1/orders/{id}

One order with its line items

Permission: read:orders. Success: 200.

FieldInTypeRequiredNotes
idpathidyesThe StoreFleet id.

Products

GET /api/v1/products

List products, most recently updated first

Delisted products are left out unless include_delisted is true.

Permission: read:products. Success: 200.

FieldInTypeRequiredNotes
limitqueryintegerRows per page, 1 to 200. Default 50.
cursorquerystringThe next_cursor from the previous page. Up to 512 characters.
in_stockquerybooleantrue: only products with stock. false: only sold out.
updated_sincequerystringOnly products changed at or after this time, ISO 8601 (2026-10-01 or 2026-10-01T09:00:00Z).
vendorquerystringOnly this brand or vendor, exact. Up to 200 characters.
include_delistedqueryboolean

GET /api/v1/products/{id}

One product with its sizes and variants

Permission: read:products. Success: 200.

FieldInTypeRequiredNotes
idpathidyesThe StoreFleet id.

Requests

GET /api/v1/inquiries

List requests, newest first

Phone numbers and emails are null unless the credential holds read:contact_info. A request a program filed never names its customer: its contact_id and converted_order_id are null, and it never appears under contact_id.

Permission: read:inquiries. Success: 200.

FieldInTypeRequiredNotes
limitqueryintegerRows per page, 1 to 200. Default 50.
cursorquerystringThe next_cursor from the previous page. Up to 512 characters.
statusquerystringOnly requests in this status. One of: open, matched, contacted, sending, won, lost, expired.
sincequerystringOnly requests submitted at or after this time, ISO 8601 (2026-10-01 or 2026-10-01T09:00:00Z).
contact_idqueryid

GET /api/v1/inquiries/{id}

One request

Permission: read:inquiries. Success: 200.

FieldInTypeRequiredNotes
idpathidyesThe StoreFleet id.

POST /api/v1/inquiries

File a customer request

Files the request in the store's Requests queue, links it to the customer by phone, and checks the shelf for a match right away. A match never texts the customer from here; staff send it from StoreFleet. A second open request for the same phone and product answers 409.

Permission: write:inquiries. Success: 201. Idempotency-Key: accepted.

FieldInTypeRequiredNotes
phonebodystringyesThe customer's mobile number. US numbers may omit +1. 7 to 40 characters.
productbodystringyesWhat they are looking for, in their words. Up to 200 characters.
sizebodystringUp to 50 characters.
first_namebodystringUp to 100 characters.
last_namebodystringUp to 100 characters.
emailbodyemailUp to 254 characters.
match_preferencebodystringexact: only that item. similar: close matches too. Default similar. One of: exact, similar.

POST /api/v1/inquiries/{id}/close

Close a request

Closes an open or matched request as lost, with a reason. A request that is sending right now is left alone (409).

Permission: write:inquiries. Success: 200. Idempotency-Key: accepted.

FieldInTypeRequiredNotes
idpathidyesThe StoreFleet id.
reasonbodystringWhy it closed. Default other. One of: bought_elsewhere, no_longer_wanted, price, out_of_stock_too_long, unreachable, duplicate, not_a_request, unable_to_source, selling_to_us, other.

POST /api/v1/inquiries/{id}/reopen

Reopen a closed request

Moves a lost request back to open and checks the shelf again. A match waits for staff; nothing is texted.

Permission: write:inquiries. Success: 200. Idempotency-Key: accepted.

FieldInTypeRequiredNotes
idpathidyesThe StoreFleet id.

Texts

GET /api/v1/messages

List texts, newest first

direction=sent (the default) lists texts the store sent, from any feature or person. direction=received lists texts customers sent in. A received text's body is null when the store keeps message bodies off.

Permission: read:messages. Success: 200.

FieldInTypeRequiredNotes
limitqueryintegerRows per page, 1 to 200. Default 50.
cursorquerystringThe next_cursor from the previous page. Up to 512 characters.
directionquerystringOne of: sent, received.
contact_idqueryidOnly texts with this customer.
sincequerystringOnly texts at or after this time, ISO 8601 (2026-10-01 or 2026-10-01T09:00:00Z).

GET /api/v1/messages/{id}

One text the store sent

Permission: read:messages. Success: 200.

FieldInTypeRequiredNotes
idpathidyesThe StoreFleet id.

POST /api/v1/messages

Text a customer from the store's number

Sends a real text through the same gate as every StoreFleet text: opted-out customers, quiet hours (9am to 9pm store time), the store's daily limit, and repeating the same words within 24 hours are all refused with 422 and a reason. A text a person typed (sent_by person) is not held by texting hours or the 24-hour window. Name the customer by contact_id or by their number in to. Requires an Idempotency-Key header. A 202 with status unknown means the text may or may not have gone out: do not retry it. Follow the text's delivery on GET /messages/{id}.

Permission: send:messages (off unless the owner ticks it). Success: 201. Idempotency-Key: required.

FieldInTypeRequiredNotes
contact_idbodyidThe customer to text. Give this or to.
tobodystringThe customer's mobile number with its country code, like +13145550100. Give this or contact_id. A number the store has never texted becomes a new customer. Up to 32 characters.
bodybodystringyesThe words, up to 1600 characters.
media_urlbodyURLOne picture to send with the words, as an https link. Up to 2048 characters.
sent_bybodystringperson: someone typed this in a conversation, so texting hours and the 24-hour same-words window do not hold it. automation (the default): every rule applies. One of: person, automation.
person_namebodystringThe name of the person who typed it, shown on the text in StoreFleet. Up to 80 characters.

Pricing

GET /api/v1/price-suggestions

List price suggestions, newest first

Defaults to the pending ones waiting for a decision.

Permission: read:reprices. Success: 200.

FieldInTypeRequiredNotes
limitqueryintegerRows per page, 1 to 200. Default 50.
cursorquerystringThe next_cursor from the previous page. Up to 512 characters.
statusquerystringDefault pending. One of: pending, approved, applied, skipped, expired, superseded.
sincequerystringOnly suggestions made at or after this time, ISO 8601 (2026-10-01 or 2026-10-01T09:00:00Z).

GET /api/v1/price-suggestions/{id}

One price suggestion

Permission: read:reprices. Success: 200.

FieldInTypeRequiredNotes
idpathidyesThe StoreFleet id.

POST /api/v1/price-suggestions/{id}/approve

Approve a price suggestion and change the live price

Writes the suggested price (or your price) to the item, which changes what shoppers see. expected_current_price guards against a price someone changed since: if the live price differs, nothing is written and you get 422. Consigned items and big changes need a person in StoreFleet and answer 422.

Permission: write:reprices. Success: 200. Idempotency-Key: accepted.

FieldInTypeRequiredNotes
idpathidyesThe StoreFleet id.
pricebodynumberYour price instead of the suggestion. More than 0 and less than 100000.
expected_current_pricebodynumberThe price you saw; refused if the live price moved. More than 0 and less than 100000.
mark_on_salebodybooleanShow the old price struck through.

POST /api/v1/price-suggestions/{id}/skip

Skip a price suggestion

Leaves the price alone and takes the suggestion out of the queue.

Permission: write:reprices. Success: 200. Idempotency-Key: accepted.

FieldInTypeRequiredNotes
idpathidyesThe StoreFleet id.
reasonbodystringUp to 300 characters.

Metrics

GET /api/v1/metrics

The store's numbers for a date range

Orders, revenue, texts sent, replies, new requests and price changes between from (inclusive) and to (exclusive). Default the last 30 days; at most 92 days.

Permission: read:metrics. Success: 200.

FieldInTypeRequiredNotes
fromquerystringStart, ISO 8601 (2026-10-01 or 2026-10-01T09:00:00Z).
toquerystringEnd, ISO 8601 (2026-10-01 or 2026-10-01T09:00:00Z).
{
  "range": {
    "from": "2026-09-01T00:00:00.000Z",
    "to": "2026-10-01T00:00:00.000Z"
  },
  "orders": 88,
  "revenue": 41200,
  "average_order": 468.18,
  "texts_sent": 640,
  "replies": 212,
  "requests": 37,
  "requests_converted": 9,
  "price_changes": 54
}

Webhooks

GET /api/v1/webhook-event-types

The events StoreFleet can send

Permission: any valid key or app. Success: 200.

No parameters.

GET /api/v1/webhook-endpoints

List the webhook endpoints this key or app made

Permission: manage:webhooks. Success: 200.

No parameters.

POST /api/v1/webhook-endpoints

Add a webhook endpoint

Returns the signing secret ONCE, in secret. Store it: StoreFleet cannot show it again (rotate it to get a new one). Every delivery is signed with it (Standard Webhooks headers webhook-id, webhook-timestamp, webhook-signature).

Permission: manage:webhooks. Success: 201. Idempotency-Key: not used; this call is never replayed.

FieldInTypeRequiredNotes
urlbodyURLyeshttps URL on your own server. Up to 2048 characters.
eventsbodylist of stringEvent types to receive. Empty or absent: every event your permissions can read. One of: inquiry.created, message.sent, message.received, order.paid, price_suggestion.created, contact.created. Up to 6 items.
descriptionbodystringUp to 200 characters.
include_contact_infobodybooleanInclude customer phones and emails in payloads. Needs read:contact_info.

GET /api/v1/webhook-endpoints/{id}

One webhook endpoint with its recent deliveries

Permission: manage:webhooks. Success: 200.

FieldInTypeRequiredNotes
idpathidyesThe StoreFleet id.

PATCH /api/v1/webhook-endpoints/{id}

Change a webhook endpoint, or switch it on or off

Permission: manage:webhooks. Success: 200. Idempotency-Key: accepted.

FieldInTypeRequiredNotes
idpathidyesThe StoreFleet id.
urlbodyURLUp to 2048 characters.
eventsbodylist of stringEvent types to receive. Empty or absent: every event your permissions can read. One of: inquiry.created, message.sent, message.received, order.paid, price_suggestion.created, contact.created. Up to 6 items.
descriptionbodystringUp to 200 characters.
include_contact_infobodyboolean
activebodybooleanfalse switches deliveries off: nothing is sent while it is off, including events still waiting. true switches them back on for new events and clears a failure switch-off.

POST /api/v1/webhook-endpoints/{id}/rotate-secret

Replace an endpoint's signing secret

Returns the new secret ONCE. Deliveries from this moment are signed with it.

Permission: manage:webhooks. Success: 200. Idempotency-Key: not used; this call is never replayed.

FieldInTypeRequiredNotes
idpathidyesThe StoreFleet id.

DELETE /api/v1/webhook-endpoints/{id}

Delete a webhook endpoint

Permission: manage:webhooks. Success: 200. Idempotency-Key: accepted.

FieldInTypeRequiredNotes
idpathidyesThe StoreFleet id.

POST /api/v1/webhook-endpoints/{id}/test

Send a signed test event now

Posts one ping event and answers with what your server replied.

Permission: manage:webhooks. Success: 200. Idempotency-Key: not used; this call is never replayed.

FieldInTypeRequiredNotes
idpathidyesThe StoreFleet id.

Questions: jake@storefleet.com