FFL Monkeydocs.fflmonkey.com
Printed edition

FFL Monkey Docs  /  For website owners and developers

FFL Checkout API

  • Version v1, additive only
  • 20 chapters in 2 parts

Find the licensed dealer a firearm ships to, record which one the buyer chose, and report the transfer, from a website of your own.

FFL Monkey Docs / docs.fflmonkey.com / FFL Checkout API / Version 1, additive only. Questions: Support in your FFL Monkey account, https://backend.fflmonkey.com/dashboard/support, or [email protected].

Part I

Start here

For the store owner. How to create the key that lets one of your systems reach your store.

  1. 1Create an API key
Chapter01

Start here · one minute in your account

Create an API key

Your point of sale, website or script proves who it is with a key. You make one in your FFL Monkey account.

1.1Every licence can have API keys

You can create API keys for any FFL Monkey licence, whichever product it is for and whichever platform your website runs on. That includes a licence you use with our WordPress plugin: you do not need a second licence or a different plan, and your keys work alongside the plugin.

The licence's product decides what a key can reach: the permissions are listed below. The licence has to be active for a key to read or change your website's data; until then those endpoints answer 403 license_not_active. On a website running the Data Feed plugin, the plugin manages the webhook, so a key can read it but cannot change it. See Webhooks.

1.2Open API keys

Sign in to your FFL Monkey account at https://backend.fflmonkey.com/dashboard/api-keys, the account you bought your licence with, and choose API keys in the menu. Keys are made there, not in wp-admin: an up-to-date plugin has no API keys tab, and a link to the old one points to your account.

  1. Your FFL Monkey account
  2. API keys
  3. Create API key

1.3Choose its store, name it and choose its access

Choose Create API key. A key belongs to one store: pick it under Store if your account has more than one. Give it a name you will recognise later, for example "Lightspeed POS" or "Inventory script". Then give it a permission for each area of your store, from No access, Read and, where there is something to change, Read and write. A Data Feed store's key can be given:

  • In-house inventory, for the Inventory API: Read and write for anything that changes your in-house stock or prices, Read for reports and dashboards, which cannot change anything.
  • Products and changes, for the Store API: Read to read your products at your prices, the category tree, the brands and product classes, and what changed.
  • Pricing rules: Read to read your pricing rules and store-wide pricing settings, Read and write to create, change, reorder, delete and preview rules and to change the settings. See Rules and restrictions.
  • Restrictions: Read to read your restrictions, Read and write to create, change, delete and preview them.
  • Distributors: Read to see which distributors you buy from and whether your credentials were accepted, and to work out how to buy an order from them, Read and write to connect and disconnect them. A key with Read and write can also switch a distributor off, so its offers stop being priced for your store: give it only to a system you trust with that.
  • Webhook: Read to see where your store's changes are sent, Read and write to set that URL, test it and remove it. A key with Read and write decides where all your store's changes go, so give it only to a system you trust with them. See Webhooks.
  • Subscription: Read to see your FFL Monkey subscription, and get a link to pay from when it will not renew.

An FFL Checkout store's key can be given Webhook and Subscription too, and these two:

  • FFL dealers: Read to find the licensed dealers a firearm may be sent to and check a licence. See FFL Checkout.
  • FFL orders: Read to read the FFL orders you reported and the licences you uploaded, Read and write to report the dealer a buyer chose, the transfer as you run it and that it happened, and to upload the licence of a dealer the registry does not have.

A key can do only what it is given, and needs at least one permission. Create one key per system, so you can revoke one without breaking the others. A store running both products holds two licences: make a key on each.

1.4Copy it straight away

Shown once
ffl_sk_live_3f9a…

An example. A real key is ffl_sk_live_ followed by 49 letters and digits.

It is shown once. We store only a fingerprint of it, so it cannot be shown again. If you lose it, revoke it and create another.

1.5Keep it safe

Good to know

  • A store can have up to 10 active keys. Create one per system, so you can revoke one without breaking the others.
  • Revoking a key stops it working on its next request. Revoke one on the API keys page; FFL Monkey support can revoke a leaked key for you too, and never makes one.
  • The list shows when each key was last used.

Keep the key on a server

It is a secret that can change your store's stock and prices. 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.

1.6Then test it

Send the key in the Authorization header of every request. Keys start with ffl_sk_live_ followed by 49 letters and digits. A key belongs to one store and can only see and change that store's rows.

GET /key is the "test connection" call. Run it with your real key on your server. A key without In-house inventory tests itself with the Store API's GET /key, which any key can call and which lists its permissions.

Part II

Reference

For whoever writes the integration. The overview, webhooks, every error code, and one chapter per endpoint with its fields, a request and a response.

  1. 2FFL Checkout
  2. 3Store API overview
  3. 4Webhooks
  4. 5Errors
  5. 6GET /dealers/search
  6. 7GET /dealers/{licenseNumber}
  7. 8POST /ffl-orders
  8. 9POST /ffl-orders/{ref}/transfer
  9. 10POST /ffl-orders/{ref}/complete
  10. 11GET /ffl-orders/{ref}
  11. 12POST /ffl-contributions
  12. 13GET /ffl-contributions/{contribution}
  13. 14GET /key
  14. 15GET /store
  15. 16GET /subscription
  16. 17GET /webhook
  17. 18PUT /webhook
  18. 19POST /webhook/test
  19. 20DELETE /webhook
Chapter02

FFL Checkout

FFL Checkout

Find the licensed dealer a firearm ships to, record which one the buyer chose, and report the transfer.

2.1What your own systems can do

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.
  • A dealer the registry does not have. Send the licence the buyer gave you with POST /ffl-contributions; FFL Monkey reads and approves it, and GET /ffl-contributions/{contribution} then answers the dealer's licence number.
  • 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.

2.2Before you start

  1. Create a key in your FFL Monkey account, with the FFL dealers and FFL orders permissions. Create an API key
  2. Read how the API works: authentication, permissions, formats and limits. Store API overview
Chapter03

Reference · Store API v1

Store API overview

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
Format
JSON in, JSON out

3.1What it reads

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.

3.2Authentication

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.

3.3Permissions

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:

PermissionLevelsWhat it opens
Products and changesNo access, ReadYour products at your prices, the category tree, the brands and product classes, and what changed: GET /products, GET /products/{id}, GET /categories, GET /manufacturers, GET /product-classes and GET /changes.
Pricing rulesNo access, Read, Read and writeFor Data Feed stores. Read: GET /pricing-rules, GET /pricing-rules/{id} and the store-wide GET /pricing-settings. Read and write also creates, changes, deletes, reorders and previews rules, and changes the settings with PATCH /pricing-settings. In GET /changes it adds pricing_rule.* and pricing_settings.*.
RestrictionsNo access, Read, Read and writeFor 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.*.
DistributorsNo access, Read, Read and writeFor 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 inventoryNo access, Read, Read and writeThe 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.
WebhookNo access, Read, Read and writeFor 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.
FFL dealersNo access, ReadFor FFL Checkout stores. The licensed dealers a firearm may be sent to: GET /dealers/search and GET /dealers/{licenseNumber}.
FFL ordersNo access, Read, Read and writeFor FFL Checkout stores. Read: GET /ffl-orders/{ref}. Read and write also reports the dealer a buyer chose, the transfer as you run it, and that it happened: POST /ffl-orders, POST /ffl-orders/{ref}/transfer and POST /ffl-orders/{ref}/complete; and uploads the licence of a dealer the registry does not have, POST /ffl-contributions, read back with GET /ffl-contributions/{contribution}. Their changes, ffl_selection.* and ffl_order.*, are sent to your webhook: GET /changes needs Products and changes, which an FFL Checkout store's key cannot be given, so it is refused (403 product_not_included) and a webhook is the only way to be told.
SubscriptionNo access, ReadFor 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.

3.4Base 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.

3.5Rate 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.

3.6Errors

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:

3.7The product

Every product the Store API answers with has this shape: the catalog's description of it, and your own price, stock and availability.

FieldTypeWhat it holds
product_idintegerThe product's id. Changes name it, and GET /products/{id} takes it.
upcstringThe product's UPC.
skustringSuch as FFL-764503026751.
titlestringThe product's name.
descriptionstring or nullHTML, already cleaned, safe to insert into a page.
resumestring or nullA one-line summary of the specifications.
manufacturerstring or nullThe manufacturer's name.
categorystring or nullThe slug of the product's category, from GET /categories.
category_pathlist of stringsThe 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_classstring or nullSuch as firearms.
attributesobjectName to value, such as "caliber": "9mm".
imageslistOne 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 placeholder true.
weightstring or nullPounds, as a decimal string.
dimensionsobjectlength, width and height in inches, as decimal strings, each null when unknown.
requires_fflbooleanMust ship to a licensed dealer. true for a product with no class: unknown means require it.
requires_sotbooleanMust ship to a dealer with special occupational tax (SOT) status.
is_ammunitionbooleanWhether it is ammunition.
pricestring or nullYour price, with your pricing rules and rounding applied. Null when no offer can be sourced, and then is_visible is false.
stockintegerHow many are in stock to sell.
sourced_fromstring or nullThe distributor the price comes from, in_house for your own stock, or null.
is_visiblebooleanWhether you sell it now. false means take it off sale.
cost_pricestring or nullThe cost on the offer the price comes from. For your own records.
map_pricestring or nullThe minimum advertised price on that offer, if it has one.
msrp_pricestring or nullThe manufacturer's suggested price on that offer, if it has one.

3.8Keep a copy of your catalog current

For a website or a point of sale that keeps its own copy of your products:

  1. Read every product you sell with GET /products, a page at a time, and keep the meta.changes_cursor of the first page.
  2. 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.
  3. 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.

3.9When 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.

3.10Have 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.

3.11Change 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.

3.12Connect 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.
  • Turning one off forgets its credentials. DELETE /distributors/{slug} switches it off and deletes them.
  • 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.

3.13FFL 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.
  • A dealer the registry does not have. Send the licence the buyer gave you with POST /ffl-contributions; FFL Monkey reads and approves it, and GET /ffl-contributions/{contribution} then answers the dealer's licence number.
  • 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.

3.14Endpoints

Every endpoint has a page with its fields and a documented example.

3.15Versioning

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.

3.16OpenAPI 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.

Chapter04

Reference · Store API v1

Webhooks

Have your store's changes sent to your own URL as they happen, instead of asking GET /changes for them, and check that each one came from FFL Monkey.

Per store
One webhook URL
Permission
Webhook, for Data Feed and FFL Checkout stores
Signed
HMAC-SHA256 in X-FFL-Signature
Answer
Any 2xx within 10 seconds

4.1Set it

A store has one webhook URL. Set it with PUT /webhook, using a key with Webhook: Read and write, which a key of a Data Feed or an FFL Checkout store can hold.

  • The secret is shown once, in the answer to the PUT that created the webhook. It signs every delivery. Changing the URL keeps it; for a new one, DELETE /webhook and set the URL again.
  • Test it before you rely on it with POST /webhook/test, which sends one signed webhook.tested event and tells you what your URL answered.
  • A store whose Data Feed plugin is connected already has its webhook. The plugin set it up and receives every change, so PUT /webhook answers 409 webhook_managed_by_plugin. GET /webhook shows it, with managed_by plugin.
  • A webhook is optional. Reading GET /changes every few minutes works as well.

4.2What is sent

Your store's own changes, never another store's, and the same for every key: a key that sets the URL decides where all of them go. For a Data Feed store:

Aboutevent_type
Products and pricesproduct.*, merchant_price.*
Pricing rulespricing_rule.*, pricing_settings.*
Restrictionsrestriction.*
Distributorsdistributor_account.*, license.distributor_selection.*
In-house inventorymerchant_inventory.*
Your licence and subscriptionlicense.activated, license.suspended, license.canceled, subscription.*

For an FFL Checkout store: its FFL orders (ffl_selection.*, ffl_order.*), and its licence and subscription, as above.

Each change is the object GET /changes returns, and its payload is the state after the change. New kinds of change may appear: ignore a type you do not know. A kind added later reaches a webhook already set, without setting it again.

4.3Each delivery

A POST to your URL with a JSON body {"events": [change]}, one change per request. It carries these headers:

HeaderWhat it is
X-FFL-EventThe change's event_type.
X-FFL-Event-IdIts event_id.
X-FFL-Delivery-IdThis delivery's id, the same on every retry. Quote it to support.
X-FFL-CursorIts cursor.
X-FFL-TimestampWhen this attempt was sent, in Unix seconds.
X-FFL-Signaturesha256= and the hex HMAC-SHA256 of {X-FFL-Timestamp}.{the raw body}, keyed with your secret.

4.4Check the signature

Before you trust a delivery, prove it came from FFL Monkey:

  1. Read the body raw, exactly as it arrived, before any JSON parsing.
  2. Join X-FFL-Timestamp, a dot and the raw body, and compute its HMAC-SHA256 with your secret, in hex. Put sha256= in front.
  3. Compare that with X-FFL-Signature using a constant-time comparison, never ==.
  4. Refuse a timestamp more than 5 minutes from your clock, so an old delivery cannot be sent to you again.

Answer a delivery that fails the check with 401, and never apply it. The code column has the check in each language.

4.5Apply each change once

  • By event_id. The same change can arrive twice, after a retry, and a change you already read with GET /changes can arrive later by webhook. Keep the event_ids you applied, and answer 200 to one you have seen.
  • Never go back. Changes can arrive out of order. Skip one whose cursor is not above the last one you applied for the same aggregate_type and aggregate_id. A merchant_price.recomputed change is a batch: its aggregate_id names the batch, and payload.products holds many products. Keep the last cursor you applied per product_id, and skip a product in it whose last applied cursor is above the change's.

4.6Retries and the pause

Answer any 2xx within 10 seconds, once the change is stored. Anything else is a failure: another status (4xx counts the same as 5xx), a redirect, or no answer in time. A failed delivery is tried again after 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours and 24 hours, seven attempts in all.

  • Degraded: after the first failure, GET /webhook says degraded. New changes are still sent, and the first 2xx makes it active again.
  • Paused: after 50 failures in a row, it pauses, with paused_reason delivery_health. Nothing is sent, and changes made while it is paused are not kept for it.
  • To resume it, fix the endpoint, then send PUT /webhook again, while your subscription is active. Then read GET /changes from your last cursor to catch up on what was not sent.
  • A webhook paused for any other reason, such as billing, is not the API's to lift.

4.7When your subscription is ending

Seven days or fewer before a subscription that is set to end, your webhook gets a subscription.ending_soon change, once for each end date. Its payload holds license_id, billing_status, ends_at and cancel_at_period_end.

It carries no link. Call GET /subscription when a person is about to renew: its renewal_url works for an hour and opens your store's billing, so treat it as a secret and never store, log or email it.

Chapter05

Reference · errors

Errors

Every error has the same shape, in the Inventory API and the Store API alike. Branch on error; show message to a person.

5.1The error envelope

FieldWhenWhat it holds
erroralwaysA code such as validation_failed. Branch on this.
messagealwaysA sentence for a person.
errorsvalidation_failedEach field, and what was wrong with it.
reasonlicense_not_activelicense_suspended, license_canceled or license_pending.
required_scopeinsufficient_scopeThe scope the key needs.
required_productproduct_not_includedThe FFL Monkey product the endpoint is part of: data_feed or ffl_checkout.
acknowledge_fieldzero_match_confirmation_requiredThe field to send as true with the same request to save anyway: acknowledge_zero_match.
wipe_shapecatalogue_wipe_confirmation_requiredexclude_everything, an exclude with no condition, or first_only_show, the store's first only-show.
confirm_tokencatalogue_wipe_confirmation_requiredThe token to send back as confirm_token with the same request to save anyway.

New codes may be added, so treat an unknown error by its HTTP status.

Every error answer, in this envelope or not, also carries the header X-FFL-Error-Reference, such as BE-7K3QX9PM. Log it with the error, and quote it when you write to us from Support in your FFL Monkey account or at [email protected]: it lets us find your request.

5.2Every error code

StatuserrorWhat it means, and what to do
401invalid_api_keyThe key is missing, malformed, unknown or revoked.What to do: Check the header, or create a new key.
403license_not_activeThe store's subscription is not active. reason is license_suspended, license_canceled or license_pending. GET /key still answers in both APIs, and says the same in license, as does the Store API's GET /store. Its GET /subscription answers too, with a link to pay from.What to do: The key is fine: the subscription needs attention.
403product_not_includedThe endpoint is part of the other FFL Monkey product: a Data Feed key asked for FFL dealers or FFL orders, or an FFL Checkout key for in-house inventory, products, pricing rules, restrictions or distributors. required_product names the product it needs.What to do: Use a key made for a licence of that product, in your FFL Monkey account.
403insufficient_scopeThe key lacks the permission the endpoint needs: a key with In-house inventory: Read tried to change something, or a key without Products and changes asked for products. required_scope names what it needs.What to do: Create a key with that permission in your FFL Monkey account.
404dealer_not_foundStore API only. No dealer in the registry has that licence number, in any form, or only an upload still pending or rejected.What to do: Check the number, or find the dealer with GET /dealers/search.
404ffl_order_not_foundStore API only. The store has no FFL order with that order number: it was never reported, or it is another store's.What to do: Report it with POST /ffl-orders, using the same order number.
404ffl_contribution_not_foundStore API only. The store has no licence upload with that id: it was never made, or it is another store's.What to do: Use the id POST /ffl-contributions answered with.
404item_not_foundInventory API only. The store has no row for that UPC.What to do: Create it with PUT or the batch endpoint first.
404product_not_foundStore API only. The store has no price for that product id, so it is not in the store's catalog.What to do: Use an id from GET /products or from a change.
404pricing_rule_not_foundStore API only. The store has no pricing rule with that id: there never was one, it was deleted, or it is another store's.What to do: Use an id from GET /pricing-rules.
404restriction_not_foundStore API only. The store has no restriction with that id: there never was one, it was deleted, or it is another store's.What to do: Use an id from GET /restrictions.
404distributor_not_foundStore API only. No distributor has that slug, or, to connect one, FFL Monkey no longer offers it.What to do: Use a slug from GET /distributors.
404webhook_not_setStore API only. The store has no webhook to read, test or remove.What to do: Set one with PUT /webhook.
404not_foundNo such address under /inventory, /products, /pricing-rules, /restrictions, /distributors, /dealers, /ffl-orders or /ffl-contributions: a product id that is not a number, say. Any other address under the base URL that is no endpoint answers 404 with a bare {"message": ""} instead, without this envelope.What to do: Check the path against the endpoint list.
405method_not_allowedThat path does not take that HTTP method.What to do: Check the method against the endpoint list.
409idempotency_key_reusedThe Idempotency-Key was already used for a different adjustment.What to do: Use a new key for each new adjustment.
409sync_pausedStore API only. The store has switched catalog sync off in its FFL Monkey screens, so its products are not served. It is not an empty catalog.What to do: Keep what you have and retry later. Once it answers again, read your products again.
409webhook_managed_by_pluginStore API only. The store's FFL Monkey Data Feed plugin is connected and manages its webhook, which already receives every change, so the API does not change, test or remove it.What to do: Nothing: the plugin gets every change. A store has one webhook URL.
410cursor_expiredStore API only. The cursor sent to GET /changes is older than the changes kept, at least 30 days.What to do: Read GET /products again and carry on from its meta.changes_cursor.
413payload_too_largeThe body is too big.What to do: Send fewer items per batch.
422validation_failedThe request broke a rule. errors lists each field and what was wrong.What to do: Fix the fields named in errors.
422zero_match_confirmation_requiredStore API only. The pricing rule or restriction would match no product in the catalog, so it was not saved. acknowledge_field names the field that saves it anyway.What to do: If you meant it (the stock is still to arrive, say), send the same request again with "acknowledge_zero_match": true. Otherwise check its conditions.
422catalogue_wipe_confirmation_requiredStore API only. The restriction would hide every product in your store (wipe_shape exclude_everything), or is your first only-show and would hide everything it does not match (first_only_show), so it was not saved.What to do: If you meant it, send the same request again with confirm_token set to the confirm_token in the answer.
422credentials_rejectedStore API only. The distributor did not accept the credentials sent to PUT /distributors/{slug}, so nothing was saved. Credentials saved before are kept.What to do: Check them with the distributor, then try again. Do not retry the same ones: a distributor may lock an account after repeated failed sign-ins.
429rate_limitedToo many requests for this key: more than 120 a minute. Four Store API calls have a limit of their own per store: 5 webhook tests a minute, 10 previews of pricing rules and restrictions together, 5 distributor connects a minute and 20 an hour, and 6 FFL licence uploads a minute. Dealer searches share your checkout's limits: 120 a minute from one address, and 5,000 a day for the store.What to do: Wait for Retry-After seconds. Use batch for many rows.
4xxbad_requestAny other problem with the request.What to do: Check the request against these docs.
500server_errorSomething went wrong on our side.What to do: Retry; if it keeps happening, write to us from Support in your FFL Monkey account, or at [email protected].
502distributor_unreachableStore API only. The distributor did not answer when FFL Monkey checked the credentials sent to PUT /distributors/{slug}: a timeout, a refused connection or an error on its side. Nothing was saved.What to do: The credentials may be right: try again in a few minutes.

5.3Retrying safely

  • 429: wait the number of seconds in Retry-After, then send the same request again. To write many rows, use POST /items/batch: it counts as one request.
  • 500: retry; if it keeps happening, write to us from Support in your FFL Monkey account, or at [email protected].
  • Adjustments: retry with the same Idempotency-Key, so a retry can never take away a unit twice. See Idempotency.
  • PUT, PATCH and batch set absolute values, so sending one twice is harmless.
Chapter06

Endpoint · GET /dealers/search

Find FFL dealers

Licensed dealers a firearm may be sent to, by ZIP, by a point, by name or by licence number, nearest first, with what you need to draw them on your own map.

GET/dealers/search
Needs FFL dealers: ReadBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

6.1Query parameters

Needs FFL dealers: Read, a permission of FFL Checkout stores. Say where with a zip or a point (latitude and longitude), never both, or which with a name or a license_number. At least one of them is required.

ParameterTypeMeaning
zipstringA 5-digit ZIP, or ZIP+4, to search around. Refused with a point.
latitudenumberWith longitude: a point to search around, more precise than a ZIP. -90 to 90.
longitudenumberWith latitude. -180 to 180.
namestringThe start of the dealer's business name or licence name, or of any word in it, in any case. 3 to 120 characters.
license_numberstringA licence number in any form: printed, with or without separators. Finds that dealer or nothing. Up to 32 characters.
radius_mintegerHow far to search from a ZIP or a point, in metres: 1 to 321869 (200 miles). Default 80467 (50 miles).
limitintegerHow many dealers at most: 1 to 100. Default 25.
only_on_fileboolean1 or true: only dealers your store already holds paperwork for. 0 or false does not filter. true and false are read in any letter case; anything else is refused.
sot_onlyboolean1 or true: only dealers with an SOT in force, who can receive NFA items: the class from the dealer's approved SOT stamp, until its tax year ends on 30 June. 0 or false does not filter. true and false are read in any letter case; anything else is refused.
verified_onlyboolean1 or true: only dealers the registry has seen accept a transfer, from a store's FFL transfer report. 0 or false does not filter. true and false are read in any letter case; anything else is refused.
  • The search FFL Monkey's dealer picker runs, with its rules. Only dealers whose licence is current (active, and expiring today or later, UTC) and of a type a firearm may be transferred to are found, and only those the registry could place on a map.
  • By a ZIP or a point, nearest first, within radius_m. A ZIP the registry cannot place answers no dealers with meta.zip_known false: "we do not know that ZIP", not "nobody near it".
  • By name or licence number, measured from the ZIP or point when you send one too, nearest first, otherwise by name. radius_m and only_on_file are for a ZIP or a point only.
  • sot_only and verified_only filter before limit, on every kind of search, so the dealers they leave are still the nearest ones.
  • Caps are refused, never cut down: a radius_m over 321869 or a limit over 100 is 422.
  • Searches share your checkout's limits: 120 a minute from one address, across every key and dealer picker searching from it, and 5,000 a day for the store. A search by license_number with limit=1 is outside the daily limit.

What it answers

data is a list of dealers, meta says what was searched, and map names the map FFL Monkey's dealer picker draws, its style and the tiles it falls back to, so a map you draw on the results can use the same ones. Each dealer:

FieldMeaning
license_numberThe licence number as the registry holds it, usually as printed on the licence.
license_keyThe licence without its expiry group, such as 1-62-037-01-01187: the same when the licence is renewed, so the value to keep, and to look the dealer up by again with GET /dealers/{licenseNumber}. Null only for a number that cannot be read.
business_nameThe dealer's business name.
licensee_nameThe name on the licence, often a person's. Sent for a search by name or licence number; null for a search by ZIP or by a point.
addressThe licensed premises, below.
phoneThe dealer's phone number, or null.
latitudeWhere to put the dealer on a map, in WGS84 degrees.
longitudeThe same, east to west.
distance_metersFrom the ZIP or point searched from, in metres; null when the search had no origin.
expiration_dateWhen the licence expires, the last day of its month, or null.
sot_classThe Special Occupational Tax class, for NFA items, when the dealer pays it, such as 3; otherwise null.
on_fileWhether your store already holds this dealer's paperwork, from a completed FFL order or an upload FFL Monkey approved. Your store's own: another store gets its own answer.
documentThe registry holds a current copy of the dealer's licence, below, or null.
sot_documentThe same for its SOT stamp, or null.
addressMeaning
line_1The street address, as ATF's list or the approved licence gives it.
line_2A second line, or null.
cityThe city.
stateThe two-letter state code.
zipThe ZIP.
document, sot_documentMeaning
availableAlways true when the object is there. Availability only, never a link to the document.
kindffl or sot.
expires_onWhen the copy held stops covering the dealer: the licence's expiry, or the end of the SOT tax year.
metaMeaning
zipThe 5-digit ZIP searched from; null for a search by a point, or by name or licence number without a ZIP.
zip_knownfalse only when the ZIP could not be placed, so an empty answer means "unknown ZIP" rather than "nobody within the radius".
countHow many dealers data holds.
mapMeaning
tile_urlA tile URL template with {z}, {x} and {y}.
attributionThe tile provider's credit, as HTML, to show on the map as its terms ask.
max_zoomThe deepest zoom the tiles go to.
style_urlA MapLibre style the picker draws in place of the tiles, or null when it draws the tiles. The tiles stay its fallback.
style_attributionThe style's credit, as HTML, to show on the map while the style draws. null when style_url is.

A page with a Content Security Policy must allow the tile host in img-src, and, to draw the style, its host in connect-src and what MapLibre itself needs (blob: workers and images).

Returns

  • 200The dealers found in data, nearest first when there is an origin, with meta and map. No dealer is still a 200.
  • 422validation_failed: no ZIP, point, name or licence number; a ZIP and a point together; or a parameter that broke a rule.

This endpoint can also answer 401 invalid_api_key, 403 license_not_active, 403 product_not_included, 403 insufficient_scope and 429 rate_limited.

Chapter07

Endpoint · GET /dealers/{licenseNumber}

Check one FFL licence

One licence, found by its number in any form: is it real, is it current, and may a firearm be sent to it.

GET/dealers/{licenseNumber}
Needs FFL dealers: ReadBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

7.1Fields

Only the licence number, in the address. Needs FFL dealers: Read, a permission of FFL Checkout stores.

  • Any form of the number finds it: printed (1-62-037-01-3F-01187), composed, without separators, in any case, or the license_key a search answered with.
  • Whatever its status. The newest registry row for the licence answers, so "expired" and "taken out of searches" are answers, not a 404. A licence a store uploaded that is still waiting for FFL Monkey's approval, or was rejected, is never answered.
  • Before you ship, check transfer_eligible and verification.current: a search only ever finds dealers for whom both are true, but a number a buyer typed may not be one.
  • It counts towards the 120 searches a minute from one address that your store's dealer picker counts towards too, and not towards the store's daily search limit.

What it answers

data is the licence in full: a dealer in the search's shape, with licensee_name always sent and distance_meters always null, plus:

FieldMeaning
statusThe registry row's status. active: searchable. expired: its licence ran out. inactive or rejected: taken out of searches by FFL Monkey. pending_verification: entered and not yet confirmed. Only active dealers appear in a search.
license_typeThe ATF licence type, two digits: 01 dealer, 02 pawnbroker, 06 ammunition manufacturer, 07 firearms manufacturer, and so on.
transfer_eligibleWhether a firearm may be transferred to a holder of this licence type.
verificationWhether the licence is real and current, below.
verificationMeaning
realThe licence comes from ATF's own list, or from a licence document a store uploaded and FFL Monkey approved.
currentThe dealer is active and its licence expires today or later (UTC). false when the expiry is not known.
sourceatf_import: ATF's list. contribution: an approved upload. operator_manual: entered by FFL Monkey staff.
last_verified_atWhen the row was last confirmed, by an ATF import or an approval, or null.

Returns

  • 200The licence, in data.
  • 404dealer_not_found: the registry has no dealer for this number, in any form, or only an upload still pending or rejected.
  • 404not_found: the address holds something that is not 5 to 32 letters, digits and hyphens.

This endpoint can also answer 401 invalid_api_key, 403 license_not_active, 403 product_not_included, 403 insufficient_scope and 429 rate_limited.

Chapter08

Endpoint · POST /ffl-orders

Report the dealer a buyer chose

Record which licensed dealer a buyer chose for one of your orders, by your own order number. Safe to repeat.

POST/ffl-orders
Needs FFL orders: Read and writeBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

8.1Fields

Needs FFL orders: Read and write, a permission of FFL Checkout stores. An FFL order is one fact about one of your orders: the dealer the buyer chose, and later whether the transfer happened. Your own system keeps the order.

FieldTypeRule
refrequiredstringYour own order number, kept as sent: the number you complete and read the order by. Up to 64 letters, digits, dots, underscores, colons and hyphens, starting with a letter or a digit. A WooCommerce order key (it starts wc_) is refused: it is the secret that opens a guest's order page.
dealer_license_numberoptionalstringThe licence number of the dealer the buyer chose, in any form, up to 32 characters: the license_number or license_key from GET /dealers/search.
dealer_snapshotoptionalobjectWhat the buyer saw of the dealer: business_name (up to 255 characters), address (512) and expires (32). Anything else is dropped, so no buyer data is kept.
optionoptionalstringdealer, the default: the buyer chose a dealer. deferred: the buyer will name one later.
  • The first report answers 201 and is announced as ffl_selection.captured.
  • Safe to repeat. The same ref again answers 200: the dealer is refreshed (the buyer may have changed it before the order settled), the status is kept, and nothing is announced again.
  • A dealer the registry does not know is recorded all the same, with dealer_resolved false.
  • Then report the transfer with POST /ffl-orders/{ref}/complete, which puts the dealer on file with your store.

What it answers

data is the order, the shape the three FFL order endpoints answer with:

FieldMeaning
refYour order number as you sent it, or, for an order your FFL Monkey checkout plugin reported, the reference it made (wc- and 32 hex characters).
statuscaptured: reported. completed: the transfer happened. More values may be added.
optiondealer or deferred, as reported.
dealer_license_numberAs you sent it, or null.
dealer_license_keyThe licence without its expiry group, the same across renewals; null when no number was sent or it could not be read.
dealer_resolvedWhether the licence number named a dealer in FFL Monkey's registry when the order was reported.
dealer_snapshotWhat the buyer saw of the dealer, below. Always its three fields, null where nothing was sent.
captured_atWhen the order was first reported.
completed_atWhen the transfer was reported, or null.
canceled_atWhen the order was cancelled, or null.
dealer_snapshotMeaning
business_nameThe dealer's name, as shown to the buyer.
addressThe dealer's address, as shown.
expiresThe dealer's licence expiry, as shown.

Returns

  • 201Reported: the order, in data.
  • 200Reported already: the order, its dealer refreshed.
  • 422validation_failed: a ref that is not an order number, a WooCommerce order key, or a field that broke a rule.

This endpoint can also answer 401 invalid_api_key, 403 license_not_active, 403 product_not_included, 403 insufficient_scope and 429 rate_limited.

Chapter09

Endpoint · POST /ffl-orders/{ref}/transfer

Report the order's FFL transfer

The transfer as your system runs it, from the request to the dealer to the parcel received, sent whole after every change. Safe to repeat.

POST/ffl-orders/{ref}/transfer
Needs FFL orders: Read and writeBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

9.1Fields

Needs FFL orders: Read and write, a permission of FFL Checkout stores. The order number goes in the address: the one you reported the order with, so the transfer joins it. It may also come first. The body is the whole transfer as it stands, sent after every change, the way FFL Monkey's checkout plugin reports the transfers it runs.

FieldTypeRule
transfer_coderequiredstringYour reference for the transfer, the one your emails and documents show. Up to 40 characters.
statusrequiredstringWhere it stands, in lowercase letters, digits and underscores, up to 24, kept as sent. Use the checkout plugin's where they fit, which your FFL Monkey screens know: needs_dealer, licence_uploaded, licence_unusable, needs_email, needs_request, waiting_dealer, declined, check_licence, form3_pending, ready_to_ship, shipped, received, pickup_form4, pickup_waiting, pickup_ready, collected, closed.
waits_onrequiredstringWho the next step waits on: merchant, dealer, buyer, atf or nobody.
needs_sotoptionalbooleanWhether an item needs a dealer with an SOT (NFA items). Default false.
orderoptionalobjectThe order, as your system has it: id, number, status, currency, total (a decimal string) and created_at.
buyeroptionalobjectfirst_name, last_name, email, phone, and the billing and shipping addresses, each line1, line2, city, state, postcode and country.
dealeroptionalobject or nullThe dealer it goes to: option (how the buyer chose: pick, upload, defer or store), license_number in any form, name, address, phone, email, request_email (where the request went), expiration_date, sot_class, contribution_id (an upload's id), registry_id, verified, transfers_verified and transfer_confirmed.
itemsoptionalarrayThe order's lines, up to 200.
items[].product_idoptionalintegerYour product's id.
items[].variation_idoptionalintegerIts variation's, if any.
items[].skuoptionalstringUp to 100 characters.
items[].nameoptionalstringUp to 255 characters.
items[].quantityoptionalintegerHow many.
items[].totaloptionalstringThe line total, a decimal string.
items[].requires_ffloptionalbooleanWhether it goes through a dealer.
items[].requires_sotoptionalbooleanWhether its dealer needs an SOT.
eventsrequiredarrayEvery event so far, 1 to 200. Send them all every time: each is kept once, by its id.
events[].idrequiredstringA UUID of yours, one per event.
events[].typerequiredstringWhat happened, in lowercase letters, digits and underscores, such as placed, request_sent, accepted, declined, licence_checked, form3_approved, shipped or received.
events[].atrequiredstringWhen, in UTC, such as 2026-10-10T14:02:00Z.
events[].actorrequiredstringbuyer, dealer, merchant or system.
events[].nameoptionalstringThe person, when known.
events[].detailoptionalobjectAnything else about it, up to 20 entries, such as a shipment's serial numbers.
  • Safe to repeat. The state is replaced by what you send, and only events with new ids are added.
  • The buyer is merged field by field. A field you leave out, an empty state or country, or a value WooCommerce anonymised (such as [deleted] or [email protected]) keeps what FFL Monkey holds, and any other value, an empty one included, replaces it. To remove a buyer's details, contact [email protected].
  • Buyer details are kept for the transfer record, shown in your FFL Monkey screens and to FFL Monkey's staff. Nothing about a transfer is sent to a webhook.
  • An accepted event by the dealer teaches FFL Monkey's registry that the dealer takes transfers.
  • The order number in the address follows the same rules as a reported ref. The body is checked first.

What it answers

data is the transfer as kept, never the buyer details it holds:

FieldMeaning
refYour order number, as sent.
transfer_codeAs sent.
statusAs sent.
waits_onAs sent.
needs_sotAs sent.
dealer_license_keyThe dealer's licence without its expiry group, or null when there is none or it cannot be read.
dealer_resolvedWhether it names a dealer in FFL Monkey's registry.
eventsHow many events are kept.
reported_atWhen it was last reported.
buyer_erased_atWhen FFL Monkey's support erased the buyer's details at your request, or null.

Returns

  • 200Kept: the transfer as it now stands, in data.
  • 422validation_failed: the body broke a rule, or the order number in the address is not one, or is a WooCommerce order key.

This endpoint can also answer 401 invalid_api_key, 403 license_not_active, 403 product_not_included, 403 insufficient_scope and 429 rate_limited.

Chapter10

Endpoint · POST /ffl-orders/{ref}/complete

Report that the transfer happened

Mark an FFL order completed once the transfer has happened. Its dealer is then on file with your store. Safe to repeat.

POST/ffl-orders/{ref}/complete
Needs FFL orders: Read and writeBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

10.1Fields

Only the order number, in the address: the ref you reported the order with. Needs FFL orders: Read and write, a permission of FFL Checkout stores.

  • Send it once the transfer has happened. The order is marked completed and announced as ffl_order.completed, and its dealer is then on file with your store: later searches say on_file true.
  • Safe to repeat. An order completed already is answered as it is, and nothing is announced again.
  • An order never reported is created, completed: the report may have been lost. It has no dealer then.
  • The order number in the path follows the same rules as a reported ref: up to 64 letters, digits, dots, underscores, colons and hyphens, never a WooCommerce order key.

Returns

This endpoint can also answer 401 invalid_api_key, 403 license_not_active, 403 product_not_included, 403 insufficient_scope and 429 rate_limited.

Chapter11

Endpoint · GET /ffl-orders/{ref}

Read one FFL order

One FFL order as FFL Monkey recorded it, found by your own order number.

GET/ffl-orders/{ref}
Needs FFL orders: ReadBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

11.1Fields

Only the order number, in the address. Needs FFL orders: Read, a permission of FFL Checkout stores.

The order as FFL Monkey recorded it, by the order number you reported it with, or by the reference your FFL Monkey checkout plugin made for an order it reported (wc- and 32 hex characters).

Returns

This endpoint can also answer 401 invalid_api_key, 403 license_not_active, 403 product_not_included, 403 insufficient_scope and 429 rate_limited.

Chapter12

Endpoint · POST /ffl-contributions

Upload a dealer's licence

The licence of a dealer the registry does not have, which a buyer gave your checkout, for FFL Monkey to read and approve.

POST/ffl-contributions
Needs FFL orders: Read and writeBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

12.1Fields

Needs FFL orders: Read and write, a permission of FFL Checkout stores. For a buyer whose dealer the registry does not have: send the scan of the dealer's licence the buyer gave your checkout, from your server, as multipart/form-data.

FieldTypeRule
filerequiredfileThe scan or photograph: PDF, PNG, JPG or WEBP, at most 10 MB.
kindoptionalstringffl, the dealer's licence, the default; or sot, their SOT stamp, for NFA items.
  • Read, then approved. FFL Monkey reads the file and approves it, by itself or by its staff, the same way it does an upload made in its own checkout picker. Ask GET /ffl-contributions/{contribution} what became of it: once approved it answers the dealer's licence number, to report the order and its transfer with.
  • 6 uploads a minute for your store, whichever key sends them, the same budget as your checkout picker's, refused ones included.

What it answers

FieldMeaning
idThe upload's id: ask GET /ffl-contributions/{id} about it.
kindffl or sot.
statusqueued: waiting to be read.

Returns

  • 202Taken, and queued to be read, in data.
  • 422validation_failed: no file, a file of another type or over 10 MB, or a kind it does not know.
  • 429rate_limited: more than 6 uploads in a minute for your store.

This endpoint can also answer 401 invalid_api_key, 403 license_not_active, 403 product_not_included, 403 insufficient_scope and 429 rate_limited.

Chapter13

Endpoint · GET /ffl-contributions/{contribution}

Read what became of an upload

An upload's status, the kinds of document found, and the dealer's licence number once it is approved.

GET/ffl-contributions/{contribution}
Needs FFL orders: ReadBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

13.1Fields

Only the upload's id, in the address: the one POST /ffl-contributions answered with. Needs FFL orders: Read, a permission of FFL Checkout stores.

Ask again every few minutes until it is decided: FFL Monkey's checkout plugin asks every 15. Never the reading itself or the file.

What it answers

FieldMeaning
idThe upload's id.
kindffl or sot.
statusqueued and processing while it is read; auto_approved or approved once the dealer is in the registry; pending_review while FFL Monkey's staff check it; rejected when it is not a licence FFL Monkey can accept; failed when it could not be read. More values may be added.
decision_reasonWhy it was refused or could not be read, to show the buyer; null otherwise.
foundThe kinds of document read in the file: ffl, sot, both, or none yet.
license_numberThe dealer's licence number once the upload is approved, to report the order and its transfer with; null before.
dealer_currentWhether that dealer is current: active and not past its expiry date. true or false once the upload is approved; null before. A licence that had lapsed is approved as an expired dealer, so ask the buyer for another dealer.
dealer_searchableWhether the dealer search finds that dealer by its licence number: current, of a licence type that may receive a transfer, and at a mapped address. true or false once the upload is approved; null before. A collector's licence, or one whose address could not be mapped, is current but not searchable, so ask the buyer for another dealer.

Returns

  • 200The upload, in data.
  • 404ffl_contribution_not_found: your store has no upload with this id. Another store's upload is the same answer as none.

This endpoint can also answer 401 invalid_api_key, 403 license_not_active, 403 product_not_included, 403 insufficient_scope and 429 rate_limited.

Chapter14

Endpoint · GET /key

Test the connection

Which key is this, what may it do, and is the store's licence serving the API. Any key can call it, so it is the "test connection" call for any integration.

GET/key
Any key can call itBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

14.1Fields

None. Any key can call it, whatever its permissions, so it is the call to make first with a new key.

It answers even when the store's subscription is not active, and says so in license. The endpoints that read your store's data then answer 403 license_not_active, so this is the call that tells your integration why it stopped.

What it answers

FieldMeaning
nameThe name you gave the key.
key_prefixThe key's first 16 characters, such as ffl_sk_live_7Hq2, as the account's API keys page lists it.
permissionsThe key's level in each permission it holds anything in, such as {"products": "read", "inventory": "read_write"}. A permission that is missing is No access. Always an object, {} when empty. New permissions may appear.
scopesThe same, as the scopes the endpoints name: 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. A Data Feed store's key holds only Data Feed's and the two both products share, an FFL Checkout store's only FFL Checkout's and those two.
license_idThe store the key belongs to.
rate_limit_per_minuteRequests this key may make a minute, across the whole API.
licenseThe store's subscription. status is active, pending, suspended or canceled. reason is null while it is active, and otherwise the reason the data endpoints refuse with: license_suspended, license_canceled or license_pending.

Returns

  • 200The calling key.

It can also answer 401 invalid_api_key and 429 rate_limited. It never answers 403: any key may call it, in any licence status.

Chapter15

Endpoint · GET /store

Read the store

The store the key belongs to, and the setup steps it has done and still has to do.

GET/store
Any key can call itBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

15.1Fields

None. Any key can call it, whatever its permissions, and it answers whatever the subscription's status. Use it to show a person what their store still has to set up.

What it answers

FieldMeaning
nameThe store's name, as the FFL Monkey screens show it.
licenseThe subscription's status and reason, as GET /key gives them.
productsA list of what the store pays for with this licence, such as ["data_feed"]: data_feed or ffl_checkout. A store running both products holds two licences, each with its own keys.
domainThe address the licence is bound to, such as https://shop.example.com, or null before it is bound.
platformThe store's platform: woocommerce for a WordPress store, bigcommerce for a store connected through the BigCommerce app, custom for a store's own website run through this API. magento is reserved. More values may be added.
pluginWhen the store's FFL Monkey plugin last reported (last_seen_at) and its version, or null if no plugin ever has. It says a plugin has talked to FFL Monkey, not that it is installed now.
webhookNull, or who set up the store's webhook in managed_by: plugin, the store's FFL Monkey plugin, or api, with PUT /webhook. More values may be added.
setupEach setup step, required steps first. See below.

Each setup step

FieldMeaning
keyWhich step. Branch on it. Today activation and subscription for every store, distributors, pricing and updates for a Data Feed store, and website_key for an FFL Checkout store: a key for FFL Monkey's ready-made dealer picker, which runs only through the FFL Checkout plugin, so it is never required. updates is done while a webhook that is not paused is told when products change; reading GET /changes works as well, and this step cannot see it, so it is never required. More steps may be added.
doneWhether the step is done.
requiredWhether the store needs it to be served. An optional step, such as a pricing rule, is worth doing but never blocks.
messageOne sentence to show a person: what is done, or what to do.

Returns

  • 200The key's store.

It can also answer 401 invalid_api_key and 429 rate_limited. It never answers 403: any key may call it, in any licence status.

Chapter16

Endpoint · GET /subscription

Read the subscription

Your FFL Monkey subscription: whether it renews, when it ends if it will, and a link to pay from when it will not renew.

GET/subscription
Needs Subscription: ReadBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

16.1Fields

None. Needs Subscription: Read, which a key of a Data Feed or an FFL Checkout store can hold.

It answers whatever the subscription's status, including while the store is suspended or canceled: that is when a store most needs the link to renew. Nothing here changes the subscription.

What it answers

FieldMeaning
statusThe subscription's status: trialing, active, past_due, unpaid, paused, canceled, incomplete or incomplete_expired, and any added later. Null when the store has no subscription.
planThe plan, as below, or null.
trial_ends_atWhen the trial ends, or null.
current_period_endWhen the period paid for ends, or null.
renewstrue when the subscription carries on into another period by itself. false when it is set to end, a payment failed (past_due), it is unpaid or has ended, or there is none.
ends_atWhen it is set to end, while it still runs. Null when it renews, has no date, or has already ended.
renewal_urlNull while it renews, and for an active store with no subscription. Otherwise a link to a page where the store renews, updates its card, pays or subscribes again. The page sends the store on to Stripe, where card details are typed.

The plan

FieldMeaning
nameSuch as Data Feed (monthly).
pricePer interval, as a string with two decimals, such as "17.99".
currencyISO 4217, lowercase, such as usd.
intervalmonth or year.

Treat renewal_url as a secret

The link works for an hour, and whoever holds it can open your store's billing. Fetch it when a person is about to use it, show it to them, and never store, log or email it.

Told before it ends

Seven days or fewer before a subscription that is set to end, your store gets a subscription.ending_soon change, once for each end date: by webhook, and in GET /changes for a key that can also read Products and changes. Its payload holds license_id, billing_status, ends_at and cancel_at_period_end, and no link: call GET /subscription when a person is about to renew. A subscription that renews is never announced.

Returns

  • 200The store's subscription, in data.

This endpoint can also answer 401 invalid_api_key, 403 insufficient_scope and 429 rate_limited. It never answers 403 license_not_active.

Chapter17

Endpoint · GET /webhook

Read the webhook

The URL your store's changes are sent to, who set it, and how its deliveries are going. Never its secret.

GET/webhook
Needs Webhook: ReadBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

17.1Fields

None. Needs Webhook: Read, which a key of a Data Feed or an FFL Checkout store can hold.

A store has one webhook, whoever set it. managed_by says who: api for one set with PUT /webhook, plugin for the one the store's FFL Monkey Data Feed plugin set up when it connected. You can read the plugin's, but the API does not change, remove or test it. The secret is never shown here. How deliveries work is on Webhooks.

What it answers

FieldMeaning
urlWhere deliveries are sent, with a POST.
managed_byapi or plugin, as above. More values may be added.
event_patternsThe kinds of change sent, as patterns where * matches anything. For a webhook set with the API, your store's own families (see What is sent); the plugin's is *.
statusactive; degraded while recent deliveries are failing and being retried; or paused, when nothing is sent.
paused_reasonNull unless status is paused. delivery_health after 50 failed deliveries in a row, which a PUT /webhook lifts once you have fixed the endpoint. Any other reason, such as billing, is not the API's to lift. More values may be added.
consecutive_failuresFailed deliveries since the last one that arrived. At 50 the webhook pauses.
last_success_atWhen a delivery last arrived, or null.
last_failure_atWhen a delivery last failed, or null.

Returns

  • 200The store's webhook, in data.
  • 404webhook_not_set: the store has no webhook. Set one with PUT /webhook.

This endpoint can also answer 401 invalid_api_key, 403 license_not_active, 403 product_not_included, 403 insufficient_scope and 429 rate_limited.

Chapter18

Endpoint · PUT /webhook

Set the webhook URL

Set the URL your store's changes are sent to as they happen. The first time, the answer carries the signing secret, shown once.

PUT/webhook
Needs Webhook: Read and writeBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

18.1Fields

Needs Webhook: Read and write, which a key of a Data Feed or an FFL Checkout store can hold. A Data Feed store's webhook is sent its products, prices, rules, restrictions, distributors and in-house inventory; an FFL Checkout store's, its FFL orders. Both are sent the licence's and the subscription's changes.

FieldTypeRule
urlrequiredstringAn https:// address, at most 255 characters, without a user name or password. Its host must resolve, and only to public internet addresses: never localhost, a private network or a reserved address.
  • One webhook URL per store. Your store's changes are sent there as they happen, so you do not have to poll GET /changes. Which changes, and what each delivery carries: Webhooks.
  • The first time it answers 201, with the webhook and its secret, which signs every delivery. The secret is shown once. Store it with your receiver. If you lose it, DELETE /webhook and set the URL again for a new one.
  • After that it answers 200: the URL changes, and the secret stays the one you hold.
  • Give the final address. Deliveries do not follow redirects, so a URL that answers 3xx fails.
  • A PUT resumes a paused webhook whose paused_reason is delivery_health, once you have fixed the endpoint, and clears its failures. Changes made while it was paused were not queued for it: read GET /changes from your last cursor to catch up.
  • A key with this permission decides where all of your store's changes go, whatever else it may do. Give it only to a system you trust with them.

If the Data Feed plugin is connected

A store whose FFL Monkey Data Feed plugin is connected already has its webhook: the plugin set it up, and it already receives every change. A PUT then answers 409 webhook_managed_by_plugin. If a store connects the plugin after setting a URL here, the plugin takes that webhook over.

Returns

  • 201Created: the store had no webhook. data is the webhook, as GET /webhook answers it, plus its secret, shown this once.
  • 200Changed: the webhook was set with the API before. No secret.
  • 409webhook_managed_by_plugin: the store's plugin manages its webhook.
  • 422validation_failed: the URL broke a rule. errors.url says which.

This endpoint can also answer 401 invalid_api_key, 403 license_not_active, 403 product_not_included, 403 insufficient_scope and 429 rate_limited.

Chapter19

Endpoint · POST /webhook/test

Send a test event

Send one signed test event to your webhook URL now, and see what your URL answered.

POST/webhook/test
Needs Webhook: Read and writeBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

19.1Fields

None. Needs Webhook: Read and write.

It sends one test event to your webhook URL now, signed and with the same headers as every delivery, so a receiver that accepts it accepts the real ones (see Check the signature). The event:

  • event_type is webhook.tested, and cursor is 0.
  • payload holds your license_id and the url it was sent to.
  • It is not one of your store's changes: it never appears in GET /changes, it is not retried, and a failed test does not count towards pausing the webhook. A test that arrives does not lift a pause either.

A paused webhook can be tested too, to check a fix before the PUT that resumes it. 5 tests a minute per store, whichever key sends them, on top of the key's own limit.

What it answers

Always 200 once something was tried: whether the test arrived is the answer.

FieldMeaning
deliveredtrue when your URL answered with any 2xx status.
statusThe HTTP status your URL answered with, or null when nothing answered.
duration_msHow long it took, in milliseconds.
errorWhat went wrong, in one sentence of ours, or null when it was delivered. What your server replied is never passed back.

Returns

  • 200The test was tried. data.delivered says whether it arrived.
  • 404webhook_not_set: the store has no webhook to test.
  • 409webhook_managed_by_plugin: the store's plugin manages its webhook.
  • 429rate_limited: more than 5 tests in a minute for this store. Wait for Retry-After seconds.

This endpoint can also answer 401 invalid_api_key, 403 license_not_active, 403 product_not_included, 403 insufficient_scope and 429 rate_limited.

Chapter20

Endpoint · DELETE /webhook

Remove the webhook

Stop sending your store's changes to your URL. GET /changes keeps working without one.

DELETE/webhook
Needs Webhook: Read and writeBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

20.1Fields

None. Needs Webhook: Read and write.

Removes a webhook set with PUT /webhook. Deliveries still waiting to be sent are dropped. GET /changes keeps working without a webhook. To get a new secret, remove the webhook and set it again.

Returns

This endpoint can also answer 401 invalid_api_key, 403 license_not_active, 403 product_not_included, 403 insufficient_scope and 429 rate_limited.

FFL Monkey FFL Checkout API, version 1. Base URL https://backend.fflmonkey.com/api/v1. Questions: Support in your FFL Monkey account, https://backend.fflmonkey.com/dashboard/support, or [email protected].