Read your store, its products at your prices, the category tree, what changed and your subscription, have changes sent to your own URL, change your pricing rules, pricing settings and restrictions, and connect your distributors, from your own systems: a point of sale, your own website or a script.
Base URL
https://backend.fflmonkey.com/api/v1
Auth
Authorization: Bearer ffl_sk_live_3f9a…Create a key
Limit
120 requests a minute, per key, shared with the Inventory API
The Store API reads what the FFL Monkey plugin puts in a store, for a store that is not on WordPress or a system beside one: your products with your own prices, stock and availability, the category tree they are filed in, and what changed since you last asked. Where these pages say your FFL Monkey screens, such as your Pricing or Exclusions screen, they mean your store's settings: in wp-admin under FFLMonkey.com, or, for a store on its own website, the licence's settings in your FFL Monkey account. It also says what state your store is in, what it still has to set up, and how its FFL Monkey subscription stands.
Your prices, as your plugin would show them. Each product comes with the price your pricing rules and rounding give it, the same rows and the same prices the plugin reads.
It reads, and changes how your store is priced and what it shows: your pricing rules and restrictions, your store-wide pricing settings, the distributors you buy from, and the URL your store's changes are sent to, its webhook. To change your in-house stock and prices, use the Inventory API. It is the same API, with the same key, the same rules and the same errors, under https://backend.fflmonkey.com/api/v1/inventory.
For an FFL Checkout store, it finds the licensed dealers a firearm may be sent to and records which one a buyer chose: FFL dealers and FFL orders.
One key, one store. A key reaches its own store and nothing else, and does only what its permissions allow.
Authentication
Send the key in the Authorization header of every request:
Authorization: Bearer ffl_sk_live_3f9a…
Keys start with ffl_sk_live_ followed by 49 letters and digits, and are made in your FFL Monkey account, on its API keys page. Create a key.
Keep the key on a server
It is a secret. Use it from your own server or a script, and never in a web page or browser code. The API does not answer browsers' cross-origin requests, on purpose.
A missing, mistyped, unknown or revoked key gets 401 invalid_api_key. If your FFL Monkey subscription is suspended, cancelled or not yet active, the endpoints that read or change your store's data answer 403 license_not_active, with the reason. GET /key and GET /store still answer, and say why, and GET /subscription answers with a link to pay from.
Permissions
A key has a permission per area of your store, chosen when you create it in your FFL Monkey account, under API keys: No access, Read, or, where there is something to change, Read and write. A key belongs to one store, and is offered the permissions of that store's product. Today a key can be given:
For Data Feed stores. Read: GET /restrictions and GET /restrictions/{id}. Read and write also creates, changes, deletes and previews them. In GET /changes it adds restriction.*.
Distributors
No access, Read, Read and write
For Data Feed stores. Read: GET /distributors, which distributors you buy from and whether your credentials were accepted, never the credentials themselves. Read and write also connects and disconnects them: PUT /distributors/{slug} and DELETE /distributors/{slug}. Read also works out how to buy an order from them: POST /sourcing-plan, which shows what each charges you. In GET /changes it adds distributor_account.* and license.distributor_selection.*.
In-house inventory
No access, Read, Read and write
The Inventory API. Read lists and reads rows, Read and write also changes them. In GET /changes it adds the changes to your in-house rows.
Webhook
No access, Read, Read and write
For Data Feed and FFL Checkout stores. Read: GET /webhook. Read and write also sets, tests and removes it: PUT /webhook, POST /webhook/test and DELETE /webhook. A key with Read and write decides where all your store's changes are sent, so give it only to a system you trust with them.
For Data Feed and FFL Checkout stores. Your FFL Monkey subscription, and a link to pay from when it will not renew: GET /subscription. With Products and changes as well, which only a Data Feed store's key can hold, GET /changes also carries your licence's status changes and subscription.ending_soon; an FFL Checkout store gets them by webhook.
A key's permissions are also listed as scopes (products:read, pricing_rules:read, pricing_rules:write, restrictions:read, restrictions:write, distributors:read, distributors:write, inventory:read, inventory:write, webhook:read, webhook:write, dealers:read, ffl_orders:read, ffl_orders:write, subscription:read). An endpoint that needs one the key does not hold answers 403 insufficient_scope, naming it in required_scope; an endpoint of the other product answers 403 product_not_included, naming that product in required_product. GET /key and GET /store need no permission: any key may call them. More permissions are offered as their endpoints are added.
Base URL and formats
https://backend.fflmonkey.com/api/v1
Responses are always JSON, with the result in data. A list also has meta.
Times are ISO 8601, in UTC: 2026-10-08T14:03:00+00:00.
Send request bodies as JSON with Content-Type: application/json.
Money comes back as a string with two decimals ("499.00"), never a number. Weights and sizes are decimal strings too. Send money as a string too ("22.50"), with at most two decimals: a JSON number is read the same, and a third decimal is refused rather than rounded.
A product is known by its product_id, a whole number. Changes name products by it.
Rate limit
Each key may make 120 requests a minute, counted across the whole API, Inventory API included. Every response to a valid key carries X-RateLimit-Limit and X-RateLimit-Remaining, refusals included, except a 404 or 405 for an address or a method the API does not have, which is answered before it is counted. Past the limit you get 429 rate_limited with a Retry-After header giving the seconds to wait.
A page of products can hold up to 500, so walking even a large catalog takes a handful of requests.
POST /webhook/test has a second limit of its own: 5 a minute for the store, whichever key calls it, because each call makes FFL Monkey send a request to your URL.
POST /pricing-rules/preview and POST /restrictions/preview share one too: 10 previews a minute for the store, the two together, whichever key calls them, because each one works part of your catalog out twice, for up to five seconds.
PUT /distributors/{slug} has its own as well: 5 a minute and 20 an hour for the store, whichever key calls it, refused ones included, because each one signs in at the distributor with your credentials, and a distributor may lock an account after repeated failed sign-ins. Disconnecting is not limited.
Each of these limits counts every request the endpoint answers, refused ones included, and on these endpoints X-RateLimit-Limit and X-RateLimit-Remaining describe whichever limit has fewer requests left, usually the endpoint's own rather than the key's 120.
Errors
Every error has the same shape as the Inventory API's. Branch on error; show message to a person. New codes may be added, so treat an unknown error by its HTTP status. These codes are the Store API's own:
The slug of every category from the root down to the product's own. It follows the tree, so never split a slug on its dots to get it.
product_class
string or null
Such as firearms.
attributes
object
Name to value, such as "caliber": "9mm".
images
list
One entry per photograph, the primary first, each with every size it comes in under variants. A product with no photograph of its own has one shared placeholder entry, with placeholdertrue.
weight
string or null
Pounds, as a decimal string.
dimensions
object
length, width and height in inches, as decimal strings, each null when unknown.
requires_ffl
boolean
Must ship to a licensed dealer. true for a product with no class: unknown means require it.
requires_sot
boolean
Must ship to a dealer with special occupational tax (SOT) status.
is_ammunition
boolean
Whether it is ammunition.
price
string or null
Your price, with your pricing rules and rounding applied. Null when no offer can be sourced, and then is_visible is false.
stock
integer
How many are in stock to sell.
sourced_from
string or null
The distributor the price comes from, in_house for your own stock, or null.
is_visible
boolean
Whether you sell it now. false means take it off sale.
cost_price
string or null
The cost on the offer the price comes from. For your own records.
map_price
string or null
The minimum advertised price on that offer, if it has one.
msrp_price
string or null
The manufacturer's suggested price on that offer, if it has one.
Keep a copy of your catalog current
For a website or a point of sale that keeps its own copy of your products:
Read every product you sell with GET /products, a page at a time, and keep the meta.changes_cursor of the first page.
Call GET /changes with that cursor. Apply each change, then save meta.next_cursor and call again from it. Every answer has one, even when nothing changed.
When meta.has_more is true, call again straight away; otherwise, a few minutes later.
A change is applied as it comes: its payload is the state after the change, never a difference, and applying one twice is harmless. Changes are kept for at least 30 days. A cursor older than that answers 410 cursor_expired: start again from step 1.
When catalog sync is switched off
A store can switch catalog sync off in its FFL Monkey screens. While it is off, GET /products and GET /products/{id} answer 409 sync_paused instead of a page: it is not an empty catalog, so keep what you have. GET /categories still answers, and GET /changes leaves out product, price and in-house inventory changes, which are not replayed later. Once GET /products answers again, read your products again.
Have changes sent to you
Instead of asking GET /changes every few minutes, a store can have each change sent to its own URL as it happens. A store has one webhook URL, set with PUT /webhook; a store whose Data Feed plugin is connected already has its webhook. Each delivery is signed, so your receiver can check it came from FFL Monkey. How webhooks work, and how to check the signature.
Change how your store is priced and what it shows
A Data Feed store's pricing rules and restrictions, the ones on its Pricing and Exclusions screens, can be read and written with the API, with the same checks and the same consequences as a save on those screens. A rule or restriction names categories, brands and product classes by the slugs GET /categories, GET /manufacturers and GET /product-classes list. Your store-wide pricing settings go with them: the default margin, for a product no rule matches, and the price rounding and MAP floor that finish every price. PATCH /pricing-settings changes them. How rules and restrictions work, and the saves you confirm.
Connect your distributors
A Data Feed store's distributors, the ones on its distributors screen, can be listed, connected and disconnected with the API. GET /distributors lists every distributor FFL Monkey offers, whether your store buys from it, and in credential_fields what it takes to connect it.
Credentials are checked with the distributor before anything is saved.PUT /distributors/{slug} signs in at the distributor with them, and only if it accepts them saves them and switches it on. A refused check (422 credentials_rejected) or one the distributor does not answer (502 distributor_unreachable) saves nothing.
They are never returned, in any answer, change or webhook: only whether they were accepted, and when.
5 connects a minute and 20 an hour per store, because a distributor may lock an account after repeated failed sign-ins.
Buying an order from them.POST /sourcing-plan takes an order's lines and where each goes, and answers every distributor carrying each line and the cheapest way to buy the lot with shipping, the plan your Data Feed order screen shows.
FFL Checkout: dealers and orders
An FFL Checkout store that sells firearms on a website of its own can do what the FFL Checkout plugin does at checkout: find the licensed dealer (an FFL) a firearm ships to, and record which one the buyer chose.
Find a dealer.GET /dealers/search is the search FFL Monkey's own dealer picker runs: by a ZIP or a point, nearest first, or by name or licence number. It finds only active dealers a firearm may be transferred to, and gives each one's coordinates for your own map.
Check one licence.GET /dealers/{licenseNumber} says whether a licence is real and current, in any form of its number. Keep a dealer's license_key: it stays the same when the licence is renewed.
Report the order.POST /ffl-orders records the dealer the buyer chose, by your own order number, and POST /ffl-orders/{ref}/complete that the transfer happened, which puts the dealer on file with your store. Both are safe to repeat. These two keep the dealer, never the buyer.
Report the transfer.POST /ffl-orders/{ref}/transfer keeps the transfer as your system runs it, from the request to the dealer to the parcel received, sent whole after every change, so it shows in your FFL Monkey screens beside the order. It keeps the buyer's details you send in it.
Dealer searches share your checkout's limits: 120 a minute from one address and 5,000 a day for the store, counted with your store's dealer picker. A lookup of one dealer by licence number is outside the daily limit.
Endpoints
Every endpoint has a page with its fields and a documented example.
This is version 1, and the /v1/ in the address is a promise: it only grows. We may add endpoints, response fields, optional request fields, error codes, permissions and values (such as a new setup step or a new kind of change), so ignore fields you do not know and treat unknown values as valid.
We will not remove or rename a field, change its type or meaning, or make an optional field required. A change that would break that is a new version at a new address.
OpenAPI document
A machine-readable description of the Store API (OpenAPI 3.1), for Postman, code generators and similar tools:
https://docs.fflmonkey.com/openapi/v1.json
The Inventory API has its own, linked from its overview.