FFL Monkeydocs.fflmonkey.com
Printed edition

FFL Monkey Docs  /  For website owners and developers

Data Feed API

  • Version v1, additive only
  • 48 chapters in 3 parts

Read your products at your prices, change your pricing rules and restrictions, connect your distributors and keep your in-house stock current, from your own systems: a point of sale, your own website, a spreadsheet or a script.

FFL Monkey Docs / docs.fflmonkey.com / Data Feed 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, and the ways to connect one.

  1. 1Create an API key
  2. 2Connect a system
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.

Chapter02

Connect a system

Connect a system

Pick the system you use. A point of sale or your own script keeps your in-house stock current through the API.

2.2Before you connect a tool through the API

  1. Create a Read and write key for this one system, so you can revoke it without breaking others. How
  2. Test the connection with GET /key from the tool you will use. See its page
  3. Make sure your rows exist. An adjustment needs the row first. Create rows with PUT, the batch endpoint or the CSV upload on the In-house inventory screen.

Part II

In-house inventory

For whoever writes the integration. What this part does, then one chapter per endpoint with its fields, a request and a response.

  1. 3Data Feed
  2. 4Inventory API overview
  3. 5GET /key
  4. 6GET /items
  5. 7GET /items/{upc}
  6. 8PUT /items/{upc}
  7. 9PATCH /items/{upc}
  8. 10POST /items/{upc}/adjust
  9. 11POST /items/batch
  10. 12DELETE /items/{upc}
Chapter03

Data Feed

Data Feed

Your products at your prices, the rules that set them, what your store hides, your distributors, and your in-house stock.

3.1What your own systems can do

3.2Before you start

  1. Create a key in your FFL Monkey account, with the permissions this system needs. Create an API key
  2. Read how the API works: authentication, permissions, formats and limits. Store API overview
Chapter04

Reference · Inventory API v1

Inventory API overview

Read and write your store's in-house inventory rows from your own systems: a point of sale, a bound book, a spreadsheet or a script.

Base URL
https://backend.fflmonkey.com/api/v1/inventory
Auth
Authorization: Bearer ffl_sk_live_3f9a… Create a key
Limit
120 requests a minute, per key
Format
JSON in, JSON out

4.1What a row does in your store

The API reads and writes the same rows as the In-house inventory screen: in wp-admin, FFLMonkey.com › Data Feed › In-house inventory, or, for a store on its own website, the licence's settings in your FFL Monkey account. Each row is one product you hold yourself, identified by its UPC, with a stock count and the price you sell it at. Anything you change here shows up on that screen, and anything changed there shows up here.

  • It competes with your distributors' offers for the same product. The cheapest offer wins, and at an equal price your in-house row wins.
  • Your price is published as you set it. Your pricing rules and default margin do not apply to in-house rows.
  • Your restrictions still apply. A rule about the product (its category, brand, product class or UPC, or your own SKU) or about stock hides your in-house row the same way it hides a distributor's offer. Rules about a distributor or a cost range do not apply to items you own.
  • A row with stock 0 is kept but not offered.
  • cost_price is for your own records. It does not change what your store shows.

A row only reaches your store when our catalog carries its UPC

Every item response says so in in_catalog (and product_id). When in_catalog is false, the row is saved and returned like any other, but there is no catalog product for it to attach to, so it does not appear in your store. It starts competing on its own as soon as a distributor supplies that UPC and the product is added to the catalog. You do not need to send it again.

If you have paused updates to your store in your FFL Monkey settings, changes you make through the API are still saved, but your store does not receive them while updates are paused. When you turn updates back on, your store is brought up to date.

4.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. A key belongs to one store and can only see and change that store's rows. Reading needs inventory:read, which a key given In-house inventory has; changing anything needs inventory:write, which only a key given In-house inventory: Read and write has. Create a key.

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.

A missing, mistyped, unknown or revoked key gets 401 invalid_api_key. If your FFL Monkey subscription is suspended, cancelled or not yet active, every request but GET /key gets 403 license_not_active, with the reason. GET /key still answers, and reports the same in license.

4.3Base URL and formats

https://backend.fflmonkey.com/api/v1/inventory

  • Send request bodies as JSON with Content-Type: application/json. Responses are always JSON.
  • Times are ISO 8601, in UTC: 2026-10-07T15:00:00+00:00.
  • Money comes back as a string with two decimals ("499.00"). You may send it as a number or a string, with at most two decimals.
  • A UPC is 1 to 32 letters, digits or hyphens, exactly as you store it. Leading zeros matter: 012345678905 and 12345678905 are different rows.

4.4Rate limit

Each key may make 120 requests a minute, counted across the whole API: its Store API requests count towards the same 120. A batch request counts as one request however many rows it carries, so use the batch endpoint for anything over a handful of rows.

Every response to a valid key carries X-RateLimit-Limit and X-RateLimit-Remaining, refusals included: a 403 counts like any other request. A 404 or 405 for an address or a method the API does not have is answered before it is counted, and carries neither header. Past the limit you get 429 rate_limited with a Retry-After header giving the seconds to wait.

4.5Errors

Every error has the same shape. Branch on error; show message to a person. New codes may be added, so treat an unknown error by its HTTP status.

4.6The item

Every item response uses this shape.

FieldTypeWhat it holds
upcstringAs you store it. Leading zeros matter.
skustring or nullYour own SKU.
stockinteger0 or more. A row with stock 0 is kept but not offered.
pricestringTwo decimals, such as "499.00". Published as you set it.
cost_pricestring or nullTwo decimals. For your own records: it does not change what your store shows.
notesstring or nullYour notes.
sourcestringHow the row was last written: manual, csv, spreadsheet (the In-house inventory screen) or api. Every write through this API sets it to api. More values may be added.
in_catalogbooleanWhether our catalog carries a product with this UPC. false: saved, but it cannot reach your store until it does.
product_idinteger or nullThe catalog product's id, or null when in_catalog is false.
created_atstringISO 8601, UTC.
updated_atstringISO 8601, UTC.

4.8Versioning

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 and values (such as a new source), 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.

4.9OpenAPI document

A machine-readable description of this API (OpenAPI 3.1), for Postman, code generators and similar tools:

https://docs.fflmonkey.com/openapi/inventory-v1.json

Chapter05

Endpoint · GET /key

Test the connection

Which key is this, and what may it do. Use it as the "test connection" call when you set up an integration.

GET/key
Needs In-house inventory: ReadBase URL https://backend.fflmonkey.com/api/v1/inventoryCounts 1 toward 120 a minute

5.1Fields

None. Any key with In-house inventory access can call it. To test a key that has only Products and changes, use the Store API's GET /key, which any key can call.

It answers even when the store's subscription is not active, and says so in license. Every other endpoint then answers 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_3f9a, to tell keys apart.
scopesEvery scope the key holds, for every permission it has, not only In-house inventory's: inventory:read, inventory:write for Read and write, and the Store API's, such as products:read or pricing_rules:write, when it has those too. The Store API's GET /key lists them as permissions as well.
license_idThe store the key belongs to.
rate_limit_per_minuteRequests this key may make a minute.
licenseThe store's subscription. status is active, pending, suspended or canceled. reason is null while it is active, and otherwise the reason the other endpoints refuse with: license_suspended, license_canceled or license_pending.

Returns

  • 200The calling key.

It can also answer 401 invalid_api_key, 403 product_not_included, 403 insufficient_scope (a key without In-house inventory) and 429 rate_limited. It never answers 403 license_not_active: license says why the other endpoints do.

Chapter06

Endpoint · GET /items

List rows

A page of rows, ordered by UPC.

GET/items
Needs In-house inventory: ReadBase URL https://backend.fflmonkey.com/api/v1/inventoryCounts 1 toward 120 a minute

6.1Query parameters

All optional. Needs read access.

ParameterTypeMeaning
pageintegerPage number, from 1.
per_pageintegerRows per page, 1 to 500. Default 100.
upcstringOnly these UPCs, comma-separated, up to 100.
updated_sincedate-timeOnly rows changed at or after this time (ISO 8601). Use Z, or URL-encode the + of an offset as %2B.
in_stocktrue / falsetrue for stock above 0, false for stock 0.

Returns

  • 200A page of rows in data, ordered by UPC, and meta with total, per_page, current_page and last_page.
  • 422validation_failed: a query parameter broke a rule.

This endpoint can also answer 401 invalid_api_key, 403 license_not_active, 403 product_not_included, 403 insufficient_scope (a key without In-house inventory) and 429 rate_limited.

Each row has the item shape.

Chapter07

Endpoint · GET /items/{upc}

Read one row

One row, found by its UPC.

GET/items/{upc}
Needs In-house inventory: ReadBase URL https://backend.fflmonkey.com/api/v1/inventoryCounts 1 toward 120 a minute
Chapter08

Endpoint · PUT /items/{upc}

Create or replace a row

Create a row, or replace it completely. Answers 201 when the row is new and 200 when it replaced one.

PUT/items/{upc}
Needs In-house inventory: Read and writeBase URL https://backend.fflmonkey.com/api/v1/inventoryCounts 1 toward 120 a minute

8.1Fields

Needs write access. The body is the whole row.

FieldTypeRule
stockrequiredintegerWhole number, 0 to 1,000,000.
pricerequirednumber or string0.01 to 9,999,999.99, at most two decimals.
cost_priceoptionalnumber, string or null0 to 9,999,999.99, at most two decimals, or null. For your records only.
skuoptionalstring or nullUp to 64 characters, or null.
notesoptionalstring or nullUp to 5,000 characters, or null.

This is a full replacement: an optional field you leave out becomes empty (null). To change one field and keep the rest, use PATCH.

Why the price must be at least 0.01

An in-house row is published at your price with no markup, and it wins ties. A row priced 0.00 with stock would put the product in your store at $0.00.

So no write through this API may leave a row with stock above 0 and a price under 0.01. The In-house inventory screen in wp-admin does accept 0.00, so you may meet a row priced 0.00. A PATCH or an adjustment that would leave it with stock is refused with 422 validation_failed on price until you give it a price. Taking its stock to 0 is always allowed.

Returns

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

Chapter09

Endpoint · PATCH /items/{upc}

Change some fields

Change some fields of an existing row and keep the rest. PATCH never creates a row.

PATCH/items/{upc}
Needs In-house inventory: Read and writeBase URL https://backend.fflmonkey.com/api/v1/inventoryCounts 1 toward 120 a minute

9.1Fields

Needs write access. Send any of the PUT fields, with the same rules; the rest are kept. Send null to clear cost_price, sku or notes; stock and price cannot be null. An empty body is refused.

FieldTypeRule
stockoptionalintegerWhole number, 0 to 1,000,000.
priceoptionalnumber or string0.01 to 9,999,999.99, at most two decimals.
cost_priceoptionalnumber, string or null0 to 9,999,999.99, at most two decimals, or null. For your records only.
skuoptionalstring or nullUp to 64 characters, or null.
notesoptionalstring or nullUp to 5,000 characters, or null.
  • PATCH never creates a row: 404 item_not_found if it does not exist.
  • If the row's price is 0.00, include a price unless the change leaves stock at 0.

Why the price must be at least 0.01

An in-house row is published at your price with no markup, and it wins ties. A row priced 0.00 with stock would put the product in your store at $0.00.

So no write through this API may leave a row with stock above 0 and a price under 0.01. The In-house inventory screen in wp-admin does accept 0.00, so you may meet a row priced 0.00. A PATCH or an adjustment that would leave it with stock is refused with 422 validation_failed on price until you give it a price. Taking its stock to 0 is always allowed.

Returns

  • 200Updated: the whole row, in data.
  • 404item_not_found: create the row with PUT or batch first.
  • 422A field broke a rule, the body was empty, or the row is priced 0.00.

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

Chapter10

Endpoint · POST /items/{upc}/adjust

Move stock up or down

Move a row's stock by an amount, for systems that know what happened ("sold one") rather than what is on the shelf.

POST/items/{upc}/adjust
Needs In-house inventory: Read and writeBase URL https://backend.fflmonkey.com/api/v1/inventoryCounts 1 toward 120 a minute

10.1Fields

Needs write access. The row must already exist.

FieldTypeRule
deltarequiredintegerA whole number from -100,000 to 100,000, not 0.
reasonoptionalstring or nullUp to 255 characters, kept with the adjustment.
Idempotency-KeyheaderstringOptional but strongly recommended. 1 to 255 printable characters, unique per adjustment. See Idempotency.
  • Stock never goes below 0. A delta that would take it lower stops at 0, and the response says so with clamped and applied_delta.
  • On a row priced 0.00, an adjustment that would leave stock above 0 is refused with 422 on price: set a price with PUT or PATCH first. An adjustment that takes its stock to 0 is allowed.

The adjustment in the response

The answer is the row as it now is, in data, and the adjustment beside it, in adjustment: {"data": {…}, "adjustment": {…}}.

FieldMeaning
deltaWhat you asked for.
applied_deltaWhat changed. Differs from delta only when the stock stopped at 0.
previous_stockStock before.
stockStock after.
clampedtrue when the stock stopped at 0.
reasonYour reason, or null.
idempotency_keyYour key, or null.
created_atWhen it happened.

Returns

  • 200Applied, or replayed (header Idempotent-Replayed: true).
  • 404item_not_found: the row must exist first.
  • 409idempotency_key_reused: the key was used for a different adjustment.
  • 422A field broke a rule, or the row is priced 0.00.

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

10.2Idempotency

PUT, PATCH and batch set absolute values, so sending one twice is harmless. An adjustment is different: if a "sold one" request times out and your system sends it again, a second -1 would take away a unit you still have.

So send an Idempotency-Key header with every adjustment: a value unique to that one event and the same on every retry of it, such as your POS sale id plus the line number. Up to 255 printable characters.

  • Same key, same UPC, same delta: nothing changes. You get the original result back, with the header Idempotent-Replayed: true. data shows the row as it is now.
  • Same key, different UPC or delta: 409 idempotency_key_reused, and nothing changes.
  • A request that failed (for example 404 because the row did not exist yet) does not use up its key. Retry it with the same key once the cause is fixed.

Keys are per store, and are remembered for as long as the adjustment log is kept.

Chapter11

Endpoint · POST /items/batch

Create or replace many rows

Create or replace up to 1,000 rows in one request, each exactly as PUT would.

POST/items/batch
Needs In-house inventory: Read and writeBase URL https://backend.fflmonkey.com/api/v1/inventoryCounts 1 toward 120 a minute

11.1Fields

Needs write access. The body is {"items": [...]} with 1 to 1,000 items. Each item is written exactly as PUT would.

FieldTypeRule
itemsrequiredarray1 to 1,000 rows.
items[].upcrequiredstring1 to 32 letters, digits or hyphens.
items[].…as PUTstock and price required; cost_price, sku, notes optional, with the PUT rules.
  • It counts as one request against the rate limit and refreshes your store's prices once for the whole batch, so it is the right call for a full re-count or a scheduled sync.
  • Each item is checked on its own: valid items are written and invalid ones are reported, with the same error shape a single PUT would give.
  • The same UPC twice in one request refuses the whole request with 422 validation_failed, and nothing is written.
  • Results come back in the order you sent the items.

Returns

  • 200Per-item results: created, updated, failed counts and results in request order.
  • 422The whole request was refused: no items, more than 1,000, or the same UPC twice.
  • 413payload_too_large: send fewer items per batch.

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

Chapter12

Endpoint · DELETE /items/{upc}

Remove a row

Remove a row. To keep a row but stop offering it, set its stock to 0 instead.

DELETE/items/{upc}
Needs In-house inventory: Read and writeBase URL https://backend.fflmonkey.com/api/v1/inventoryCounts 1 toward 120 a minute

Part III

Products, prices and distributors

For whoever writes the integration. The overview, how rules and restrictions work, webhooks, every error code, and one chapter per endpoint.

  1. 13Store API overview
  2. 14Rules and restrictions
  3. 15Webhooks
  4. 16Errors
  5. 17GET /products
  6. 18GET /products/{id}
  7. 19GET /categories
  8. 20GET /manufacturers
  9. 21GET /product-classes
  10. 22GET /changes
  11. 23GET /pricing-rules
  12. 24GET /pricing-rules/{id}
  13. 25POST /pricing-rules
  14. 26PATCH /pricing-rules/{id}
  15. 27DELETE /pricing-rules/{id}
  16. 28POST /pricing-rules/reorder
  17. 29POST /pricing-rules/preview
  18. 30GET /pricing-settings
  19. 31PATCH /pricing-settings
  20. 32GET /restrictions
  21. 33GET /restrictions/{id}
  22. 34POST /restrictions
  23. 35PATCH /restrictions/{id}
  24. 36DELETE /restrictions/{id}
  25. 37POST /restrictions/preview
  26. 38GET /distributors
  27. 39PUT /distributors/{slug}
  28. 40DELETE /distributors/{slug}
  29. 41POST /sourcing-plan
  30. 42GET /key
  31. 43GET /store
  32. 44GET /subscription
  33. 45GET /webhook
  34. 46PUT /webhook
  35. 47POST /webhook/test
  36. 48DELETE /webhook
Chapter13

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

13.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.

13.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.

13.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.

13.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.

13.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.

13.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:

13.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.

13.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.

13.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.

13.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.

13.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.

13.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.

13.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.

13.14Endpoints

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

13.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.

13.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.

Chapter14

Reference · Store API v1

Rules and restrictions

How your pricing rules choose a product's price, and how your restrictions choose what your store shows, so that what you write with the API does what you meant.

Pricing rules
The first match sets the price
Restrictions
Any one exclude hides
Permissions
Pricing rules, Restrictions, for Data Feed stores
Preview
10 a minute per store

14.1Pricing rules: the first match wins

Your rules are checked in order, lowest sort_order first, and on a tie the older rule. For each product, the first enabled rule that matches it sets its markup, and no other rule is applied: rules never add up. A product no rule matches is priced at cost plus your default margin, if one is set, and otherwise at cost: one of your store-wide pricing settings, with the price rounding and the MAP floor.

  • A rule matches when every condition it has matches, and a condition matches when any one of its values does: categories (each takes in the ones below it), brands, product classes, distributors and UPCs, then the cost range and the MAP and MSRP gates. An empty condition matches anything.
  • The price is the base the rule names (apply_markup_price_to), plus markup_percentage of it, plus markup_value_fixed, rounded to the cent and never below zero. Each offer is priced on its own, and the cheapest one your restrictions allow is the one your store sells, after your MAP floor and price rounding.
  • Your own stock is never priced by rules. An in-house row sells at the price you set on it.
  • So a rule's place is part of what it means. Put a narrow rule above a general one, or the general one takes its products. A new rule goes directly above the first rule broader than it, and POST /pricing-rules/reorder sets the whole order.

14.2Restrictions: any one of them hides

  • Inside one restriction, every condition must match, and inside one condition any of its values will do. Categories handguns and rifles with the brand glock match Glock handguns and Glock rifles, and nothing else.
  • Any switched-on exclude that matches hides it. One is enough, and order never matters, so restrictions have no reorder.
  • Only-shows work the other way round. Once your store has a switched-on only_show, a product must match at least one of them to be shown. An exclude wins over an only-show.
  • They judge each offer, not the product. A product hidden at one distributor can still be sold from another that no restriction matches. A restriction that names distributors or a cost range never hides your own stock.
  • min_stock turns with the kind: on an exclude it matches an offer with fewer than that many in stock, on an only-show one with at least that many, so both hide low stock.
  • A switched-off restriction (disabled) is kept, and hides nothing.

14.3Saves you confirm

Some saves are held back with a 422 until you say you mean them. Nothing is saved: send the same body again with the field the answer names.

AnswerWhenSend again with
zero_match_confirmation_requiredA pricing rule or restriction whose conditions name no product in the catalog. Not asked of one with no condition or with a cost range, whose products cannot be counted, nor of a switched-off restriction."acknowledge_zero_match": true
catalogue_wipe_confirmation_requiredA restriction that would empty your store: an exclude with no condition (wipe_shape exclude_everything), or your first switched-on only-show (first_only_show).confirm_token, taken from the answer

The empty-store question comes first, so a first only-show that matches nothing needs both. Take the token from the answer rather than writing it into your code. An only-show with no condition is not confirmed but refused, since it would restrict nothing.

14.4Slugs and money

  • Conditions name rows by slug. categories, manufacturers and product_classes take the slugs GET /categories, GET /manufacturers and GET /product-classes list; distributors takes distributor slugs, as a product's sourced_from names them.
  • A slug that names nothing is refused by its place (manufacturers.1) and never dropped: a dropped slug would widen the rule. Slugs are checked once every other field passes, so a body with a field error and an unknown slug is told about the field first; then every slug that names nothing is named at once.
  • A row deleted since a rule was saved reads "(deleted)". The rule still holds it and matches nothing through it. Sending "(deleted)" back is refused: leave that condition out of a PATCH to keep it, or send the slugs you mean.
  • Money is a decimal string. Send at most two decimals, best as a string such as "22.50"; a JSON number is read the same, and a third decimal is refused, never rounded. It comes back as a string with two decimals.

14.5Preview before you save

POST /pricing-rules/preview and POST /restrictions/preview take the body you would save, or an id and the fields you would change, and say what saving it would do: how many products it names, and up to 25 real products worked out both ways. They write nothing. Your store may make 10 previews a minute, the two kinds together, whichever key asks.

14.6What a save sets off

  • Your store is worked out again, exactly as after a change on your FFL Monkey screens. New prices, and products leaving or coming back, arrive as merchant_price.recomputed changes.
  • The save itself is announced as pricing_rule.created, .updated, .deleted or .reordered, or restriction.created, .updated or .deleted: to your webhook, and in GET /changes for a key that can read them.
  • A create is not idempotent. A create retried after a lost answer makes a copy: list them, and delete the copy.
Chapter15

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

15.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.

15.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.

15.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.

15.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.

15.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.

15.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.

15.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.

Chapter16

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.

16.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.

16.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.

16.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.
Chapter17

Endpoint · GET /products

List products

Your products at your prices: everything you sell, a page at a time, or exactly the ones you name by id or UPC.

GET/products
Needs Products and changes: ReadBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

17.1Query parameters

All optional. Needs Products and changes: Read. Send at most one of cursor, ids and upc.

ParameterTypeMeaning
cursorintegerThe meta.next_cursor of the page before. Leave it out for the first page. Not a changes cursor.
limitintegerProducts per page, 1 to 500. Default 100.
idsstringOnly these product ids, comma-separated, up to 100.
upcstringOnly these UPCs, comma-separated, up to 100.
  • Everything you sell, a page at a time. Without ids or upc you get what you sell now, ordered by product_id. Call again with cursor set to meta.next_cursor until meta.has_more is false.
  • Particular products. With ids or upc you get exactly those, including ones you no longer sell, with is_visible false: that is how you learn to take one off sale. One the store has no price for is left out.
  • Two of cursor, ids and upc together are refused with 422, and so is an id or a UPC that is not one.

What it answers

data is a list of products, each in the product shape, and meta says where the walk is:

FieldMeaning
next_cursorSend it as cursor for the next page. Null on the last page, and on a lookup by ids or upc.
has_moretrue while there is another page.
totalHow many products you sell. Approximate: the number can move during a long walk.
changes_cursorWhere to read GET /changes from after a walk. Keep the one from the walk's first page, so every change made during the walk is read again; applying one twice is harmless.

Returns

  • 200A page of products in data, and meta.
  • 409sync_paused: the store has switched catalog sync off. Not an empty catalog: keep what you have and retry later.
  • 422validation_failed: a query parameter 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.

Chapter18

Endpoint · GET /products/{id}

Read one product

One product at your price, whether or not you sell it now.

GET/products/{id}
Needs Products and changes: ReadBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

18.1Fields

Only the product_id, in the address. Needs Products and changes: Read.

You get the product whether or not you sell it now: is_visible false means take it off sale. Use it when a change names a product, or GET /products?ids= for several at once.

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.

Chapter19

Endpoint · GET /categories

List categories

The category tree your products are filed in, every parent before its children.

GET/categories
Needs Products and changes: ReadBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

19.1Fields

None. Needs Products and changes: Read.

Every category, in one flat list ordered so that a parent always comes before its children (by depth, then sort_order), so you can build the tree in one pass. A product's category and category_path are slugs from here. It is the same tree for every store, and it is served even while catalog sync is switched off.

Each category

FieldMeaning
slugThe category's name in this API: a product's category and category_path, and the categories of a pricing rule or restriction, name it by its slug. FFL Monkey staff can change a slug; the category stays the same category, so read the tree again rather than keeping it for good.
nameThe category's name for people.
descriptionCopy for the category's own page, or null.
parentThe parent's slug, or null at a root.
depth0 at a root, 1 for its children, and so on.
sort_orderOrder among the parent's children.

Returns

  • 200The tree, in data.

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 · GET /manufacturers

List brands

The brands, by slug, with the name a product's manufacturer field shows. Pricing rules and restrictions name brands by these slugs.

GET/manufacturers
Needs Products and changes: ReadBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

20.1Query parameters

All optional. Needs Products and changes: Read.

ParameterTypeMeaning
cursorstringThe meta.next_cursor of the page before. Leave it out for the first page.
limitintegerBrands per page, 1 to 1000. Default 100.
  • Active brands, a page at a time. There are thousands, so call again with cursor set to meta.next_cursor until meta.has_more is false. They come in byte order of the slug.
  • The slug is the brand's name in this API. Pricing rules and restrictions name brands by it, in manufacturers. A product's manufacturer shows the name.
  • A brand merged into another is left out, and a rule or restriction that sends its slug is refused. One saved before the merge still reads back with that slug: to change its brands, send the slug of the brand it was merged into, or leave manufacturers out of a PATCH to keep what it holds.
  • FFL Monkey staff can change a slug; the brand stays the same brand. Read the list again rather than keeping it for good.
  • The same for every store, and served while catalog sync is switched off.

Each brand

FieldMeaning
slugThe brand's name in this API, such as glock.
nameWhat a product's manufacturer field shows, such as Glock.

Where the walk is

FieldMeaning
next_cursorSend it as cursor for the next page. Null on the last page.
has_moretrue while there is another page.

Returns

  • 200A page of brands in data, and meta.
  • 422validation_failed: a query parameter 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.

Chapter21

Endpoint · GET /product-classes

List product classes

Every product class, by slug. A product's product_class is one of them, and pricing rules and restrictions name classes by it.

GET/product-classes
Needs Products and changes: ReadBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

21.1Fields

None. Needs Products and changes: Read.

Every product class, by slug, in one short list, in the order your FFL Monkey screens show them. A product's product_class is one of these slugs, and pricing rules and restrictions name classes by it, in product_classes. It is the same list for every store, and it is served while catalog sync is switched off.

Each class

FieldMeaning
slugWhat a product's product_class names the class by, such as firearms.
nameThe class's name for people, such as Firearms.

Returns

  • 200Every class, in data.

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

Chapter22

Endpoint · GET /changes

Read what changed

What changed in your store since a cursor, oldest first. Keeps a copy of your catalog current.

GET/changes
Needs Products and changes: ReadBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

22.1Query parameters

All optional. Needs Products and changes: Read.

ParameterTypeMeaning
cursorintegerThe meta.next_cursor of the answer before, or the meta.changes_cursor of GET /products. Without it the answer has no changes, and meta.next_cursor is where to start from now.
limitintegerChanges per answer, 1 to 500. Default 100.
  1. Read your products with GET /products and keep the meta.changes_cursor of its first page.
  2. Call GET /changes?cursor= with it. Apply each change, then call again with meta.next_cursor. Every answer has one, even when nothing changed, and it moves on past changes that are not for your key, so save it each time.
  3. When meta.has_more is true, call again straight away; otherwise, a few minutes later.

Each change

Exactly what a webhook delivery carries. Apply each once, by event_id; reading one twice is harmless.

FieldMeaning
event_idThe change's identity: the same change read twice has the same event_id.
event_typeWhat kind of change, such as merchant_price.recomputed. See the table below. Ignore a type you do not know.
event_versionThe version of the payload's shape.
license_idYour store's licence, or null for a change to the shared catalog (product.*).
aggregate_typeWhat kind of thing changed, such as product.
aggregate_idWhich one.
cursorThe change's position. It only grows.
occurred_atWhen it happened.
payloadThe state after the change, never a difference: apply it as it is.
metadataWhere the change came from. For information, not for deciding whether to apply it.

The changes your key gets

Permissionevent_typeWhat to do
Products and changesmerchant_price.*merchant_price.recomputed: your price, stock or availability moved. payload.products holds each product's new price, stock, sourced_from and is_visible; apply them as they are, and take a product with is_visible false off sale.
Products and changesproduct.*product.created and product.updated: a product in the shared catalog changed, its title, images or category, say. These are about the whole catalog, not only your products: fetch the product with GET /products?ids=, and one you have no price for comes back empty.
Pricing rulespricing_rule.*pricing_rule.created, pricing_rule.updated and pricing_rule.deleted: a rule was saved or removed, whoever did it; pricing_rule.reordered: the order changed, and payload.ordered_ids is the new one. A payload names categories, brands and product classes by internal ids, not slugs: read the rule with GET /pricing-rules/{id} (payload.pricing_rule_id), unless it was deleted. Your prices follow as merchant_price.recomputed. Only for a key that also has Pricing rules.
Pricing rulespricing_settings.*pricing_settings.updated: a store-wide pricing setting changed, on your Pricing screen or with PATCH /pricing-settings. Its payload holds all three, default_margin, price_rounding and enforce_map_floor, as GET /pricing-settings answers them. Your prices follow as merchant_price.recomputed. Only for a key that also has Pricing rules.
Restrictionsrestriction.*restriction.created, restriction.updated and restriction.deleted: a restriction was saved or removed, whoever did it. Its payload names catalog rows by internal ids too: read it with GET /restrictions/{id} (payload.restriction_id), unless it was deleted. What it hides follows as merchant_price.recomputed. Only for a key that also has Restrictions.
Distributorsdistributor_account.*, license.distributor_selection.*distributor_account.connected: credentials for a distributor were saved, or replaced, whoever did it; distributor_account.disconnected: they were deleted. payload.distributor_slug names the distributor, and no payload carries a credential. license.distributor_selection.changed: the distributors you buy from changed, and payload.selected_distributors is the new list. Your prices follow as merchant_price.recomputed. Only for a key that also has Distributors.
In-house inventorymerchant_inventory.*merchant_inventory.updated: a row of your in-house inventory was written or removed. Only for a key that also has In-house inventory.
Subscriptionlicense.activated, license.suspended, license.canceledYour licence started, stopped or ended serving: payload.status is the new status. Read GET /subscription for the detail. Only for a key that also has Subscription.
Subscriptionsubscription.*subscription.ending_soon: your subscription is set to end within seven days. payload.ends_at is when. Call GET /subscription for the link to renew; the change carries none.

You never get another store's changes. If your store switches catalog sync off, product, price and in-house inventory changes are left out while it is off, and are not replayed later, and meta.sync_paused is true. Once it is false again, read your products again. To have changes sent to you instead of asking for them, set a webhook.

What it answers

FieldMeaning
next_cursorWhere to read from next. On every answer, and it moves on even when no change was for your key.
has_moretrue when the answer is full: read again from next_cursor straight away.
sync_pausedtrue while your store has switched catalog sync off, so product, price and in-house inventory changes were left out of this answer. They are not replayed: when it turns false, read GET /products again.

Returns

  • 200The changes after the cursor, oldest first, in data, and meta.
  • 410cursor_expired: changes are kept for at least 30 days, and this cursor is older than what is kept. Read GET /products again and carry on from its meta.changes_cursor.
  • 422validation_failed: a query parameter 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.

Chapter23

Endpoint · GET /pricing-rules

List pricing rules

Your pricing rules, in the order they are checked: for each product, the first enabled rule that matches it sets its price.

GET/pricing-rules
Needs Pricing rules: ReadBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

23.1Fields

None. Needs Pricing rules: Read, a permission of Data Feed stores.

Your pricing rules, the ones on your Pricing screen, first checked first. For each product, the first enabled rule that matches it sets its price, and no other rule is applied; a product no rule matches is priced at cost plus your default margin, if one is set. How rules and restrictions work together: Rules and restrictions.

Each rule

FieldMeaning
idThe rule's id, which GET, PATCH and DELETE /pricing-rules/{id} take.
nameThe rule's name, as your Pricing screen shows it.
markup_percentagePercent added to the base, a string with two decimals such as "22.00", or null for none.
markup_value_fixedDollars added after the percentage, a string with two decimals, or null for none.
apply_markup_price_toThe base: cost, the distributor's cost; msrp; map_then_cost, MAP when it is above cost, otherwise cost; or map_then_msrp, MAP when it is above MSRP or there is no MSRP, otherwise MSRP. An offer missing the base it names is marked up from its cost.
apply_only_when_has_maptrue: the rule skips a product whose offer has no MAP above its cost.
apply_only_when_has_msrptrue: the rule skips a product whose offer has no MSRP.
enforce_map_floortrue never prices below MAP, false may, null follows your store's setting.
categoriesCategory slugs from GET /categories. A category takes in the ones below it. Empty: any.
manufacturersBrand slugs from GET /manufacturers. Empty: any.
product_classesProduct class slugs from GET /product-classes. Empty: any.
distributorsDistributor slugs, as a product's sourced_from names them. Empty: any.
upcsUPCs. Empty: any.
from_priceThe lowest distributor cost the rule applies to, two decimals, or null for no lower bound.
to_priceThe highest distributor cost it applies to, or null for no upper bound.
sort_orderIts place: lower is checked first, and on a tie the older rule. Change it with POST /pricing-rules/reorder.
disabledtrue: the rule is kept, and never applied.

A rule matches a product when every condition it has matches, and a condition matches when any one of its values does. Money is a string with two decimals.

A condition that reads "(deleted)"

A category, brand or product class deleted since the rule was saved is shown as "(deleted)", which is never a slug. The rule still holds it, and matches nothing through it. Sending "(deleted)" back is refused, so to change such a rule, leave that condition out of the PATCH (the rule keeps what it holds) or send the slugs you mean.

Returns

  • 200Every rule, in data, in the order they are checked.

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

Chapter24

Endpoint · GET /pricing-rules/{id}

Read one pricing rule

One pricing rule, found by its id.

GET/pricing-rules/{id}
Needs Pricing rules: ReadBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

24.1Fields

Only the rule's id, in the address. Needs Pricing rules: Read.

One rule, in the shape GET /pricing-rules lists it, conditions by slug. Use it when a pricing_rule.* change names a rule: the change's payload names categories, brands and classes by internal ids, which this API never shows.

Returns

  • 200The rule, in data.
  • 404pricing_rule_not_found: your store has no rule with that id. Another store's rule is the same answer.
  • 404not_found: the id in the address is not a number.

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

Chapter25

Endpoint · POST /pricing-rules

Create a pricing rule

Add a pricing rule. It goes directly above the first rule broader than it, and your prices are worked out again.

POST/pricing-rules
Needs Pricing rules: Read and writeBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

25.1Fields

Needs Pricing rules: Read and write. Send the rule's fields as GET /pricing-rules shows them, without id and sort_order. A field left out is null, false or an empty list.

FieldTypeRule
namerequiredstringUp to 120 characters.
markup_percentageoptionalstring, number or nullPercent added to the base, -100 to 999.99, at most two decimals, or null for none. Best sent as a string, such as "22.50".
markup_value_fixedoptionalstring, number or nullDollars added after the percentage, -100000 to 100000, at most two decimals, or null for none.
apply_markup_price_torequiredstringThe base the markup is added to: cost, msrp, map_then_cost or map_then_msrp.
apply_only_when_has_mapoptionalbooleantrue skips a product whose offer has no MAP above its cost.
apply_only_when_has_msrpoptionalbooleantrue skips a product whose offer has no MSRP.
enforce_map_flooroptionalboolean or nulltrue never prices below MAP, false may, null follows your store's setting.
categoriesoptionallist of stringsCategory slugs from GET /categories; each takes in the categories below it. Empty or left out: any.
manufacturersoptionallist of stringsBrand slugs from GET /manufacturers. Empty or left out: any.
product_classesoptionallist of stringsProduct class slugs from GET /product-classes. Empty or left out: any.
distributorsoptionallist of stringsDistributor slugs, as a product's sourced_from names them. Empty or left out: any.
upcsoptionallist of stringsUPCs, each up to 32 characters. Empty or left out: any.
from_priceoptionalstring, number or nullThe lowest distributor cost the rule applies to, 0 to 9999999.99, at most two decimals, or null for no lower bound.
to_priceoptionalstring, number or nullThe highest distributor cost it applies to, on the same terms, or null for no upper bound.
disabledoptionalbooleantrue keeps the rule and never applies it.
auto_placeoptionalbooleanDefault true: directly above the first rule that is broader than this one. false: last.
acknowledge_zero_matchoptionalboolean Send true to save a rule that matches no product in the catalog, after a 422 zero_match_confirmation_required.

Each list takes at most 1,000 entries. Money with a third decimal is refused, never rounded. A rule's place is not a field: sort_order is refused. Move a rule with POST /pricing-rules/reorder.

  • Where it goes. With auto_place (the default) it goes directly above the first rule that is broader than it, as your Pricing screen places a new rule, so a narrow rule is checked before a general one. false puts it last. To choose another place, reorder afterwards.
  • Slugs are checked, not guessed. A slug that names nothing, or a brand merged into another, is 422 validation_failed, named by its place, such as manufacturers.1. It is never dropped: a dropped slug would widen the rule. Slugs are checked once every other field passes, so fix the fields errors names first; then every slug that names nothing is named at once.
  • What follows: exactly what follows a rule saved on the Pricing screen. Your prices are worked out again and arrive as merchant_price.recomputed changes, and the rule is announced as pricing_rule.created.

A rule that would match nothing

A rule whose conditions name no product in the catalog is not saved: the answer is 422 zero_match_confirmation_required, with the sentence your Pricing screen shows and acknowledge_field naming the field to send. If you meant it (the stock is still to arrive, say), send the same body again with "acknowledge_zero_match": true. It is not asked for a rule with no condition, or one with a cost range, whose products cannot be counted. Preview a rule to see what it names before you save it.

A retried create makes a second rule

A create is not idempotent. If a request timed out, list the rules before you send it again, and delete a copy you find with DELETE /pricing-rules/{id}. Two identical rules price the same, since the first one wins.

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.

Chapter26

Endpoint · PATCH /pricing-rules/{id}

Change a pricing rule

Change some fields of a rule and keep the rest, its place included. There is no PUT.

PATCH/pricing-rules/{id}
Needs Pricing rules: Read and writeBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

26.1Fields

The rule's id in the address, and in the body only the fields to change, at least one. Needs Pricing rules: Read and write.

FieldTypeRule
nameoptionalstringUp to 120 characters.
markup_percentageoptionalstring, number or nullPercent added to the base, -100 to 999.99, at most two decimals, or null for none. Best sent as a string, such as "22.50".
markup_value_fixedoptionalstring, number or nullDollars added after the percentage, -100000 to 100000, at most two decimals, or null for none.
apply_markup_price_tooptionalstringThe base the markup is added to: cost, msrp, map_then_cost or map_then_msrp.
apply_only_when_has_mapoptionalbooleantrue skips a product whose offer has no MAP above its cost.
apply_only_when_has_msrpoptionalbooleantrue skips a product whose offer has no MSRP.
enforce_map_flooroptionalboolean or nulltrue never prices below MAP, false may, null follows your store's setting.
categoriesoptionallist of stringsCategory slugs from GET /categories; each takes in the categories below it. Empty: any. Sent, it replaces what the rule holds; left out, the rule keeps it.
manufacturersoptionallist of stringsBrand slugs from GET /manufacturers. Empty: any. Sent, it replaces what the rule holds; left out, the rule keeps it.
product_classesoptionallist of stringsProduct class slugs from GET /product-classes. Empty: any. Sent, it replaces what the rule holds; left out, the rule keeps it.
distributorsoptionallist of stringsDistributor slugs, as a product's sourced_from names them. Empty: any. Sent, it replaces what the rule holds; left out, the rule keeps it.
upcsoptionallist of stringsUPCs, each up to 32 characters. Empty: any. Sent, it replaces what the rule holds; left out, the rule keeps it.
from_priceoptionalstring, number or nullThe lowest distributor cost the rule applies to, 0 to 9999999.99, at most two decimals, or null for no lower bound.
to_priceoptionalstring, number or nullThe highest distributor cost it applies to, on the same terms, or null for no upper bound.
disabledoptionalbooleantrue keeps the rule and never applies it.
acknowledge_zero_matchoptionalboolean Send true to save a rule that matches no product in the catalog, after a 422 zero_match_confirmation_required.

Each list takes at most 1,000 entries. Money with a third decimal is refused, never rounded. A rule's place is not a field: sort_order is refused, and so is auto_place. Move a rule with POST /pricing-rules/reorder.

  • Everything you do not send is kept: the rule's place, its switch, its MAP floor setting and every condition you leave out. A condition you send replaces the old one, and [] means any.
  • There is no PUT, on purpose: a PUT resets what it is not sent, so one that left out a condition would widen the rule to every product, and one that left out its place would move it.
  • A condition that reads "(deleted)" cannot be sent back. Leave it out to keep what the rule holds, or send the slugs you mean.
  • Its place is not a field. Move it with POST /pricing-rules/reorder.
  • A change that leaves the rule matching nothing is held back as a create is, until you send "acknowledge_zero_match": true. It is asked again on each later change while the rule still matches nothing.
  • What follows: your prices are worked out again, and the rule is announced as pricing_rule.updated.

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.

Chapter27

Endpoint · DELETE /pricing-rules/{id}

Delete a pricing rule

Remove a rule. The products it priced fall to the next rule that matches them, or to your default margin.

DELETE/pricing-rules/{id}
Needs Pricing rules: Read and writeBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

27.1Fields

Only the rule's id, in the address. Needs Pricing rules: Read and write.

The products the rule priced fall to the next rule that matches them, or to your default margin. Your prices are worked out again, and the rule is announced as pricing_rule.deleted. To keep a rule but stop applying it, PATCH it with "disabled": true instead.

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.

Chapter28

Endpoint · POST /pricing-rules/reorder

Reorder pricing rules

Put every rule in a new order, first checked first. Send the whole list.

POST/pricing-rules/reorder
Needs Pricing rules: Read and writeBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

28.1Fields

Needs Pricing rules: Read and write.

FieldTypeRule
ordered_idsrequiredlist of integersThe id of every rule your store has, each once, first checked first: the ids GET /pricing-rules lists. 1 to 1,000 of them.
  • The whole list, every time. A list that leaves a rule out, names one twice or names one your store does not have is 422 validation_failed on ordered_ids, and nothing moves.
  • Every price is worked out again, since any rule may now be the first to match a product, and the change is announced as pricing_rule.reordered.
  • To move one rule, read the list, move its id, and send the list back.

Returns

  • 200Every rule, in data, in the new order.
  • 422validation_failed: the list is not every rule, each once.

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

Chapter29

Endpoint · POST /pricing-rules/preview

Preview a pricing rule

See what a new rule, or a change to one, would do to your prices before you save it. Saves nothing.

POST/pricing-rules/preview
Needs Pricing rules: Read and writeBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

29.1Fields

Needs Pricing rules: Read and write, like the save it stands in for. It is the preview your Pricing screen shows, and it saves nothing: no rule, no price and no change are written or announced. Send one of two bodies:

  • A new rule: the body you would send to POST /pricing-rules, checked as a create is. It is previewed where that create would put it.
  • A change to a rule: rule_id and the fields you would send to PATCH /pricing-rules/{id}, laid over the rule as the PATCH would lay them. rule_id alone previews the rule as it is.
FieldTypeRule
rule_idoptionalintegerA stored rule to preview a change to. With it, every other field is optional and is laid over the rule as a PATCH would lay it. Without it, the body is a new rule's.
nameoptionalstringUp to 120 characters. Required for a new rule.
markup_percentageoptionalstring, number or nullPercent added to the base, -100 to 999.99, at most two decimals, or null for none. Best sent as a string, such as "22.50".
markup_value_fixedoptionalstring, number or nullDollars added after the percentage, -100000 to 100000, at most two decimals, or null for none.
apply_markup_price_tooptionalstringThe base the markup is added to: cost, msrp, map_then_cost or map_then_msrp. Required for a new rule.
apply_only_when_has_mapoptionalbooleantrue skips a product whose offer has no MAP above its cost.
apply_only_when_has_msrpoptionalbooleantrue skips a product whose offer has no MSRP.
enforce_map_flooroptionalboolean or nulltrue never prices below MAP, false may, null follows your store's setting.
categoriesoptionallist of stringsCategory slugs from GET /categories; each takes in the categories below it. Empty or left out: any.
manufacturersoptionallist of stringsBrand slugs from GET /manufacturers. Empty or left out: any.
product_classesoptionallist of stringsProduct class slugs from GET /product-classes. Empty or left out: any.
distributorsoptionallist of stringsDistributor slugs, as a product's sourced_from names them. Empty or left out: any.
upcsoptionallist of stringsUPCs, each up to 32 characters. Empty or left out: any.
from_priceoptionalstring, number or nullThe lowest distributor cost the rule applies to, 0 to 9999999.99, at most two decimals, or null for no lower bound.
to_priceoptionalstring, number or nullThe highest distributor cost it applies to, on the same terms, or null for no upper bound.
disabledoptionalbooleantrue keeps the rule and never applies it.
auto_placeoptionalbooleanDefault true: directly above the first rule that is broader than this one. false: last. Refused with rule_id.
acknowledge_zero_matchoptionalbooleanAccepted, and changes nothing: a preview is never refused for matching nothing, it says so in zero_match.

Each list takes at most 1,000 entries. Money with a third decimal is refused, never rounded. A rule's place is not a field: sort_order is refused. Move a rule with POST /pricing-rules/reorder.

10 previews a minute per store, pricing rules and restrictions together, whichever key asks, on top of the key's own limit: each one works part of your catalog out twice, for up to five seconds.

What it answers

How many products the rule names, and up to 25 real products worked out both ways, those that would change first.

FieldMeaning
scope_countHow many products in the catalog its conditions name, counted up to limits.max_count. The whole catalog when narrowed is false.
narrowedfalse when its conditions could not be counted (no condition at all, or a cost range), so scope_count is the whole catalog.
scope_cappedThe count stopped at limits.max_count: there are more.
scope_exactfalse when a condition is judged per offer (MAP or MSRP only, a cost range, a stock floor), so scope_count is an upper bound and the samples are the truth.
catalog_countProducts in the catalog, up to limits.max_count.
sampledProducts worked out both ways, up to 200.
changedOf those, how many would change.
timed_outThe five seconds ran out while sampling: sampled is what was done.
zero_matchIt names no product in the catalog, so saving it would ask for acknowledge_zero_match.
samplesUp to limits.max_samples products, those that would change first. See below.
limitsThe limits the answer was worked out under: max_count (100,001), max_samples (25) and timeout_seconds (5).

Each sample

FieldMeaning
product_idThe product, as GET /products/{id} takes it.
titleIts name.
upcIts UPC, or null.
image_urlIts thumbnail, or null.
beforeThe product as your store stands now. See below.
afterThe product once the rule is saved.
changedIts price, whether it is sold, why not, or where it comes from would change.

Each side, before and after

FieldMeaning
priceThe price your store would sell it at, or null when it would not be sold.
visibleWhether your store would sell it.
hidden_reasonWhy it would not be sold, such as restricted, no_offers or no_price, or null when it would.
sourced_fromThe distributor it would come from, in_house for your own stock, or null.
applied_rule_idThe pricing rule that would price it, or null: your default margin, or the rule you are previewing while it has no id yet.
cost_priceThe winning distributor offer's cost; null for your own stock.
map_priceThe winning offer's MAP, or null.
msrp_priceThe winning offer's MSRP, or null.
this_ruletrue when the rule you are previewing would price it: the new rule, or the stored rule you are changing, which keeps its own id. Pricing rule previews only.

Returns

  • 200What saving it would do, in data. Nothing was saved.
  • 404pricing_rule_not_found: your store has no rule with that rule_id.
  • 422validation_failed: a field broke a rule, as it would on the save.
  • 429rate_limited: more than 10 previews 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.

Chapter30

Endpoint · GET /pricing-settings

Read the pricing settings

Your store-wide pricing settings: the margin for a product no rule matches, how every price is rounded, and the MAP floor.

GET/pricing-settings
Needs Pricing rules: ReadBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

30.1Fields

None. Needs Pricing rules: Read, a permission of Data Feed stores.

The three store-wide settings every price is built with, beside your pricing rules: the card above the rules on your Pricing screen. They come under Pricing rules because they are the rules' fallback and finish, not a permission of their own. When one changes, here or on the screen, GET /changes and your webhook carry pricing_settings.updated with all three.

What it answers

FieldMeaning
default_marginThe margin for a product no pricing rule matches: a percentage over cost, as a decimal string such as "25". Null: no margin, so such a product is priced at cost.
price_roundingHow every price is rounded, as a decimal string. Below 1 it is an ending, rounded up to: "0.99" makes 122.50 into 122.99. From 1 it is a step, rounded to the nearest: "5" makes 123.40 into 125.00. Null: no rounding.
enforce_map_floortrue: never publish a price below the distributor's MAP. A pricing rule's own enforce_map_floor overrides it for the products the rule prices. On by default, but stores that predate the setting were given it off, so read it rather than assume.

How a price is built

  1. The markup. A product no enabled rule matches is its cost plus default_margin, rounded to the cent. A product a rule matches is marked up by the rule instead.
  2. The rounding of price_rounding, on every price.
  3. The MAP floor, last, so rounding can never leave a price under MAP. It only ever raises a price.

Worked through for an offer no rule matches, costing 100.00 with a MAP of 119.00: "25" makes it 125.00, "0.99" makes that 125.99, which is above MAP, so the price is 125.99. With a margin of "10" it would be 110.00, then 110.99, and the floor would raise it to 119.00.

Returns

  • 200The settings, in data.

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

Chapter31

Endpoint · PATCH /pricing-settings

Change the pricing settings

Change any of the three store-wide pricing settings and keep the others. Your prices are worked out again.

PATCH/pricing-settings
Needs Pricing rules: Read and writeBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

31.1Fields

Only the settings to change, at least one. Needs Pricing rules: Read and write. What each setting does: GET /pricing-settings.

FieldTypeRule
default_marginoptionalstring, number or null0 to 10000: digits with an optional decimal part, such as "22.5", with no sign and no exponent. Best sent as a string, for an exact value; a JSON number is read as the string. Null clears it: no margin.
price_roundingoptionalstring, number or null0 to 1000, on the same terms: an ending below 1, such as "0.99", or a step from 1, such as "5". Null clears it: no rounding.
enforce_map_flooroptionalbooleantrue or false, never null: cleared, it would be on, which true says.
  • Everything you do not send is kept.
  • All or nothing. Every setting sent is checked before any is saved, so a 422 validation_failed changes nothing. A body with none of the three is refused, named body.
  • Money as decimal strings. The answer gives each setting back as the decimal string it is saved as, never as a number.
  • What follows: exactly what follows a change on your Pricing screen. Each setting that changes is announced as pricing_settings.updated, carrying all three as GET /pricing-settings answers them, in GET /changes for a key with Pricing rules and to your webhook, and your whole catalogue is worked out again, the new prices arriving as merchant_price.recomputed changes. A setting sent with the value it already has changes nothing and is not announced.

Returns

  • 200All three settings as they are now, in data.
  • 422validation_failed: a setting broke a rule, or the body has none of the three (named body). Nothing was saved.

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

Chapter32

Endpoint · GET /restrictions

List restrictions

Your restrictions, the rules that hide products from your store. Order never matters.

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

32.1Fields

None. Needs Restrictions: Read, a permission of Data Feed stores.

Your restrictions, the rules on your Exclusions screen, oldest first. A product is hidden from your store when any switched-on exclude matches it, or when you have a switched-on only_show and none of them matches it. An exclude wins over an only-show, and order never matters, so restrictions have no reorder. How they combine, offer by offer: Rules and restrictions.

Each restriction

FieldMeaning
idThe restriction's id, which GET, PATCH and DELETE /restrictions/{id} take.
nameIts name, as your Exclusions screen shows it.
restriction_typeexclude hides the products it matches. only_show: once your store has a switched-on one, every product that no switched-on only-show matches is hidden.
distributorsDistributor slugs, as a product's sourced_from names them: it applies to those distributors' offers only. Empty: any.
categoriesCategory slugs from GET /categories. A category takes in the ones below it. Empty: any.
manufacturersBrand slugs from GET /manufacturers. Empty: any.
product_classesProduct class slugs from GET /product-classes. Empty: any.
upcsUPCs. Empty: any.
skusSKUs: a distributor's, matched against its offer, or your own in-house one. Empty: any.
min_stockOn an exclude, it matches an offer with fewer than this in stock; on an only-show, one with at least this many, so both hide low stock. Null: any.
from_priceThe lowest distributor cost it matches, two decimals, or null for no lower bound.
to_priceThe highest distributor cost it matches, or null for no upper bound.
disabledtrue: the restriction is kept, and hides nothing.

A restriction matches an offer when every condition it has matches, and a condition matches when any one of its values does. Money is a string with two decimals. A category, brand or product class deleted since the restriction was saved reads "(deleted)", as on a pricing rule, and sending it back is refused.

Returns

  • 200Every restriction, in data, oldest first.

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

Chapter33

Endpoint · GET /restrictions/{id}

Read one restriction

One restriction, found by its id.

GET/restrictions/{id}
Needs Restrictions: ReadBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

33.1Fields

Only the restriction's id, in the address. Needs Restrictions: Read.

One restriction, in the shape GET /restrictions lists it, conditions by slug. Use it when a restriction.* change names one: the change's payload names categories, brands and classes by internal ids, which this API never shows.

Returns

  • 200The restriction, in data.
  • 404restriction_not_found: your store has no restriction with that id. Another store's is the same answer.
  • 404not_found: the id in the address is not a number.

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

Chapter34

Endpoint · POST /restrictions

Create a restriction

Add a restriction. One that would empty your store, or match nothing, is saved only once you confirm it.

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

34.1Fields

Needs Restrictions: Read and write. Send the restriction's fields as GET /restrictions shows them, without id. A field left out is null, false or an empty list.

FieldTypeRule
namerequiredstringUp to 120 characters.
restriction_typerequiredstringexclude hides what it matches; only_show hides what no switched-on only-show matches.
distributorsoptionallist of stringsDistributor slugs, as a product's sourced_from names them: it applies to those distributors' offers, never to your own stock. Empty or left out: any.
categoriesoptionallist of stringsCategory slugs from GET /categories; each takes in the categories below it. Empty or left out: any.
manufacturersoptionallist of stringsBrand slugs from GET /manufacturers. Empty or left out: any.
product_classesoptionallist of stringsProduct class slugs from GET /product-classes. Empty or left out: any.
upcsoptionallist of stringsUPCs, 6 to 14 digits each. A code that can never match (a spreadsheet's scientific notation, a leading apostrophe) is refused. Empty or left out: any.
skusoptionallist of stringsSKUs of up to 64 characters: a distributor's, matched against its offer, or your own in-house one. A code that can never match is refused. Empty or left out: any.
min_stockoptionalinteger or null0 to 1000000. On an exclude, it matches an offer with fewer than this in stock; on an only-show, one with at least this many. Null: any.
from_priceoptionalstring, number or nullThe lowest distributor cost it matches, 0 to 9999999.99, at most two decimals, or null for no lower bound.
to_priceoptionalstring, number or nullThe highest distributor cost it matches, on the same terms, and never below from_price, or null for no upper bound.
disabledoptionalbooleantrue keeps the restriction and hides nothing.
confirm_tokenoptionalstring After a 422 catalogue_wipe_confirmation_required: the confirm_token from that answer, to save a restriction that empties your store anyway.
acknowledge_zero_matchoptionalboolean Send true to save a restriction that matches no product in the catalog, after a 422 zero_match_confirmation_required.

Each list takes at most 1,000 entries. Money with a third decimal is refused, never rounded. Name distributors in distributors: distributor_slug and distributor_slugs are refused.

  • Checked, not guessed. A slug that names nothing is 422 validation_failed, named by its place, as on a pricing rule; so is a to_price below from_price, and a UPC or SKU that can never match.
  • An only-show with no condition is refused (422 validation_failed, on categories): it would match every product and restrict nothing.
  • What follows: exactly what follows a restriction saved on your Exclusions screen. Your store is worked out again (products leave or come back as merchant_price.recomputed changes), and the restriction is announced as restriction.created.

A restriction that would empty your store

Two shapes hide everything in one save: an exclude with no condition, which matches every product, and your first switched-on only_show, which hides everything it does not match. Neither is saved until you confirm it. The answer is 422 catalogue_wipe_confirmation_required, with the sentence your Exclusions screen shows, wipe_shape (exclude_everything or first_only_show) and a confirm_token. If you meant it, send the same body again with that confirm_token.

A restriction that would match nothing

One whose conditions name no product in the catalog is held back as a pricing rule is: 422 zero_match_confirmation_required, until you send "acknowledge_zero_match": true. A switched-off restriction is never asked. The empty-store question is asked first, so a first only-show that matches nothing needs both.

A retried create makes a second restriction

A create is not idempotent. If a request timed out, list the restrictions before you send it again, and delete a copy you find with DELETE /restrictions/{id}. Two identical restrictions hide the same products.

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.

Chapter35

Endpoint · PATCH /restrictions/{id}

Change a restriction

Change some fields of a restriction and keep the rest. There is no PUT.

PATCH/restrictions/{id}
Needs Restrictions: Read and writeBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

35.1Fields

The restriction's id in the address, and in the body only the fields to change, at least one. Needs Restrictions: Read and write.

FieldTypeRule
nameoptionalstringUp to 120 characters.
restriction_typeoptionalstringexclude hides what it matches; only_show hides what no switched-on only-show matches.
distributorsoptionallist of stringsDistributor slugs, as a product's sourced_from names them: it applies to those distributors' offers, never to your own stock. Empty: any. Sent, it replaces what the restriction holds; left out, it keeps it.
categoriesoptionallist of stringsCategory slugs from GET /categories; each takes in the categories below it. Empty: any. Sent, it replaces what the restriction holds; left out, it keeps it.
manufacturersoptionallist of stringsBrand slugs from GET /manufacturers. Empty: any. Sent, it replaces what the restriction holds; left out, it keeps it.
product_classesoptionallist of stringsProduct class slugs from GET /product-classes. Empty: any. Sent, it replaces what the restriction holds; left out, it keeps it.
upcsoptionallist of stringsUPCs, 6 to 14 digits each. A code that can never match (a spreadsheet's scientific notation, a leading apostrophe) is refused. Empty: any. Sent, it replaces what the restriction holds; left out, it keeps it.
skusoptionallist of stringsSKUs of up to 64 characters: a distributor's, matched against its offer, or your own in-house one. A code that can never match is refused. Empty: any. Sent, it replaces what the restriction holds; left out, it keeps it.
min_stockoptionalinteger or null0 to 1000000. On an exclude, it matches an offer with fewer than this in stock; on an only-show, one with at least this many. Null: any.
from_priceoptionalstring, number or nullThe lowest distributor cost it matches, 0 to 9999999.99, at most two decimals, or null for no lower bound.
to_priceoptionalstring, number or nullThe highest distributor cost it matches, on the same terms, and never below from_price, or null for no upper bound.
disabledoptionalbooleantrue keeps the restriction and hides nothing.
confirm_tokenoptionalstring After a 422 catalogue_wipe_confirmation_required: the confirm_token from that answer, to save a restriction that empties your store anyway.
acknowledge_zero_matchoptionalboolean Send true to save a restriction that matches no product in the catalog, after a 422 zero_match_confirmation_required.

Each list takes at most 1,000 entries. Money with a third decimal is refused, never rounded. Name distributors in distributors: distributor_slug and distributor_slugs are refused.

  • Everything you do not send is kept: its switch, its distributors and every condition you leave out. A condition you send replaces the old one, and [] means any. There is no PUT, for the reason a pricing rule has none.
  • A condition that reads "(deleted)" cannot be sent back. Leave it out to keep what the restriction holds, or send the slugs you mean.
  • The checks a create meets are made on the restriction as it would be saved. Emptying the one condition of an only-show is refused, and switching on an exclude with no condition asks for its confirm_token. A change to a restriction that already matches nothing is asked about again.
  • What follows: your store is worked out again, and the restriction is announced as restriction.updated.

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.

Chapter36

Endpoint · DELETE /restrictions/{id}

Delete a restriction

Remove a restriction. The products it hid come back, unless another restriction hides them.

DELETE/restrictions/{id}
Needs Restrictions: Read and writeBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

36.1Fields

Only the restriction's id, in the address. Needs Restrictions: Read and write.

The products it hid come back, unless another restriction hides them. Your store is worked out again, and the restriction is announced as restriction.deleted. To keep a restriction but stop it hiding anything, PATCH it with "disabled": true instead.

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.

Chapter37

Endpoint · POST /restrictions/preview

Preview a restriction

See what a new restriction, or a change to one, would hide before you save it. Saves nothing.

POST/restrictions/preview
Needs Restrictions: Read and writeBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

37.1Fields

Needs Restrictions: Read and write, like the save it stands in for. It saves nothing: no restriction, no price and no change are written or announced. Send one of two bodies:

  • A new restriction: the body you would send to POST /restrictions, checked as a create is. An only-show with no condition, or a from_price above the to_price, is refused, as a save refuses it.
  • A change to a restriction: restriction_id and the fields you would send to PATCH /restrictions/{id}, laid over it as the PATCH would lay them.
FieldTypeRule
restriction_idoptionalintegerA stored restriction to preview a change to. With it, every other field is optional and is laid over the restriction as a PATCH would lay it. Without it, the body is a new restriction's.
nameoptionalstringUp to 120 characters. Required for a new restriction.
restriction_typeoptionalstringexclude hides what it matches; only_show hides what no switched-on only-show matches. Required for a new restriction.
distributorsoptionallist of stringsDistributor slugs, as a product's sourced_from names them: it applies to those distributors' offers, never to your own stock. Empty or left out: any.
categoriesoptionallist of stringsCategory slugs from GET /categories; each takes in the categories below it. Empty or left out: any.
manufacturersoptionallist of stringsBrand slugs from GET /manufacturers. Empty or left out: any.
product_classesoptionallist of stringsProduct class slugs from GET /product-classes. Empty or left out: any.
upcsoptionallist of stringsUPCs, 6 to 14 digits each. A code that can never match (a spreadsheet's scientific notation, a leading apostrophe) is refused. Empty or left out: any.
skusoptionallist of stringsSKUs of up to 64 characters: a distributor's, matched against its offer, or your own in-house one. A code that can never match is refused. Empty or left out: any.
min_stockoptionalinteger or null0 to 1000000. On an exclude, it matches an offer with fewer than this in stock; on an only-show, one with at least this many. Null: any.
from_priceoptionalstring, number or nullThe lowest distributor cost it matches, 0 to 9999999.99, at most two decimals, or null for no lower bound.
to_priceoptionalstring, number or nullThe highest distributor cost it matches, on the same terms, and never below from_price, or null for no upper bound.
disabledoptionalbooleantrue keeps the restriction and hides nothing.
confirm_tokenoptionalstringAccepted, and changes nothing.
acknowledge_zero_matchoptionalbooleanAccepted, and changes nothing: a preview is never refused for matching nothing, it says so in zero_match.

Each list takes at most 1,000 entries. Money with a third decimal is refused, never rounded. Name distributors in distributors: distributor_slug and distributor_slugs are refused.

The two confirmations a save asks for are not asked here: the preview is how you find out what they are about. 10 previews a minute per store, pricing rules and restrictions together, whichever key asks.

What it answers

The shape of a pricing rule preview, without this_rule: there applied_rule_id is the pricing rule that prices the product. A product it would hide has visible false and hidden_reason restricted after. For an only-show, what it hides is everything it does not name, so catalog_count against scope_count is the number to read, and the samples are drawn from the whole catalog as well.

Returns

  • 200What saving it would do, in data. Nothing was saved.
  • 404restriction_not_found: your store has no restriction with that restriction_id.
  • 422validation_failed: a field broke a rule, as it would on the save.
  • 429rate_limited: more than 10 previews 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.

Chapter38

Endpoint · GET /distributors

List distributors

Every distributor FFL Monkey offers, whether your store buys from it, what it takes to connect it, and whether your credentials were accepted. Never a credential.

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

38.1Fields

None. Needs Distributors: Read, a permission of Data Feed stores.

Every distributor FFL Monkey offers, by name, and your store's connection to each, as your distributors screen shows them. It never shows a credential, nor any part of one: only whether yours are saved, and whether the distributor accepted them.

Each distributor

FieldMeaning
slugThe distributor's name in the API: the address of PUT and DELETE /distributors/{slug}, a product's sourced_from, and the values of a pricing rule's or restriction's distributors.
nameIts name, as your FFL Monkey screens show it.
enabledtrue while your store buys from it: its offers are priced for your store only while it is on.
credential_fieldsWhat to send to connect it, in the order to ask a person for them. See below. Empty when it cannot be connected through the API yet.
credentialsnone: nothing is saved for it, though it may still be on, from before connecting needed credentials. verified: the distributor accepted the saved credentials when they were sent, at credentials_checked_at. not_verified: saved before FFL Monkey checked credentials with the distributor.
credentials_checked_atWhen the distributor accepted them, or null unless credentials is verified.

What to send to connect one

Each entry of credential_fields is one value PUT /distributors/{slug} needs. Distributors ask for different things, so build the form from this list rather than assuming an email and a password.

FieldMeaning
keyThe name to send its value under: credentials.<key>.
labelWhat to call it to a person, such as Email.
secrettrue for a password or a key: do not show it on screen.
hintMore help with the field when its label is not enough, or null.

Returns

  • 200Every distributor, in data, by name.

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

Chapter39

Endpoint · PUT /distributors/{slug}

Connect a distributor

Send your credentials with a distributor. They are checked with the distributor first, and only if it accepts them are they saved and the distributor switched on.

PUT/distributors/{slug}
Needs Distributors: Read and writeBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

39.1Fields

The distributor's slug in the address, from GET /distributors, and your own credentials with it in the body. Needs Distributors: Read and write, a permission of Data Feed stores.

FieldTypeRule
credentialsrequiredobjectOne string for each entry of the distributor's credential_fields, under that entry's key, such as {"email": "…", "password": "…"}. Every one filled in, each up to 1,000 characters. Keys that are not in credential_fields are ignored.
  • Checked with the distributor before anything is saved. FFL Monkey signs in at the distributor with them, which can take up to 15 seconds. Only if it accepts them are they saved and the distributor switched on (enabled true).
  • A refused or unreachable check saves nothing. Nor does a field that is missing or blank, which is refused before the distributor is asked anything. Credentials saved before are kept when a new set is refused.
  • Never returned. No answer, change or webhook carries a credential, nor any part of one: the answer says only that they are verified, and when.
  • Connected already? Sending it again checks the new credentials and replaces the old ones.
  • What follows: exactly what follows a connect on your distributors screen. Saving the credentials is announced as distributor_account.connected; switching the distributor on, as license.distributor_selection.changed, and your products and prices are worked out again.

5 a minute and 20 an hour per store

Each PUT signs in at the distributor, and a distributor may lock an account after repeated failed sign-ins. So your store may send 5 a minute and 20 an hour, whichever key sends them, refused ones counted, on top of the key's own limit. Fix the credentials rather than retrying the same ones. Disconnecting is not limited.

Returns

  • 200Connected: the distributor as GET /distributors now shows it, in data, with credentials verified.
  • 404distributor_not_found: no distributor FFL Monkey offers has that slug.
  • 422validation_failed: a credential is missing or blank, named as credentials.<key>; or credentials itself when the body has none at all, or when the distributor's credential_fields is empty, as it cannot be connected through the API yet. The distributor was not asked.
  • 422credentials_rejected: the distributor did not accept them. Nothing was saved.
  • 429rate_limited: more than 5 in a minute, or 20 in an hour, for this store. Wait for Retry-After seconds.
  • 502distributor_unreachable: the distributor did not answer the check. Nothing was saved. The credentials may be right: try again in a few minutes.

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

Chapter40

Endpoint · DELETE /distributors/{slug}

Disconnect a distributor

Switch a distributor off and forget your credentials for it. Safe to repeat.

DELETE/distributors/{slug}
Needs Distributors: Read and writeBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

40.1Fields

Only the distributor's slug, in the address. Needs Distributors: Read and write.

Switches the distributor off, so its offers stop being priced for your store, and forgets your credentials for it: to turn it back on, connect it again with PUT /distributors/{slug} and send them again. It answers 204 whether or not there was anything to switch off or forget, so it is safe to repeat, and a distributor FFL Monkey no longer offers can still be disconnected. It asks the distributor nothing, so it is not limited.

Announced as distributor_account.disconnected when credentials were deleted, and license.distributor_selection.changed when the distributor was on, in which case your products and prices are worked out again, as on your distributors screen.

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.

Chapter41

Endpoint · POST /sourcing-plan

Work out how to buy an order

Every distributor you buy from that carries each line of an order, and the cheapest way to buy the lot with shipping, a box at a time. Nothing is written.

POST/sourcing-plan
Needs Distributors: ReadBase URL https://backend.fflmonkey.com/api/v1Counts 1 toward 120 a minute

41.1Fields

Needs Distributors: Read. The body is the order: where its lines go, and the lines. Nothing is written, and the plan is worked out afresh on every call, so ask again just before you buy.

FieldTypeRule
store_stateoptionalstring or nullYour store's own state, two letters, which a line going through your store must be allowed to reach.
destinationsrequiredarrayWhere the order's lines go: 1 to 8.
destinations[].keyrequiredstringYour name for it, up to 32 characters, each once in the body.
destinations[].kindrequiredstringdealer, the dealer the buyer chose; address, the order's shipping address; store, the buyer collects at your store; or pending, a dealer not chosen yet, so no state is checked.
destinations[].stateoptionalstring or nullIts state, two letters.
linesrequiredarrayThe order's lines: 1 to 100.
lines[].keyrequiredstringYour own id for the line, up to 32 characters. It comes back unchanged.
lines[].product_idrequiredintegerThe product_id GET /products answers with.
lines[].quantityrequiredinteger1 to 9,999.
lines[].destinationrequiredstringThe key of one of the body's destinations. One the body does not declare is 422.
pinsoptionalarrayThe distributor you chose for a line: the plan buys that line from it, choosing its route and every other line around it. Up to 100, each line at most once.
pins[].linerequiredstringA line's key. One the body does not send is 422.
pins[].distributorrequiredstringThe distributor's slug, as a carrier names it. One that can no longer fill the line is ignored, and the line is planned as if not pinned.
  • Priced at what you pay: each offer's cost, not your price, and each distributor's published shipping terms, a box at a time. Lines one distributor sends to one place travel in one box.
  • Two ways to send a line: direct, drop-shipped by the distributor, or via_store, to your store and on from there, with both legs priced: the distributor's box to you and your own box on, from FFL Monkey's estimate of a store's shipping. A line for a buyer collecting at your store goes to_store, one leg.
  • The cheapest plan, not each line's cheapest offer: shipping is per box, so the plan prices whole combinations. cheapest_items is each line's cheapest offer on its own, priced the same way, and savings what the plan saves against it.
  • Every selected distributor is listed for each line, in stock or not, once. One that cannot fill it says why in refusal. The plan weighs every offer, because a distributor can list a product under two SKUs that ship differently; it lists the offer its plan buys.
  • What another distributor would cost: each carrier that can fill a line says in if_chosen what the order would cost with that distributor, every other line kept with its distributor and every offer and route chosen again. Pin every line that way and the plan you get is that one, while costs and stock hold. An order too large to price every alternative has if_chosen only on the carriers the plan uses.
  • A pin that cannot be kept is set aside: when the distributor you pinned can no longer fill the line, the plan buys it elsewhere and the line says pin_ignored.

What it answers

data is the plan:

FieldMeaning
quoted_atWhen it was worked out. Offers and stock move: ask again before you buy.
linesEach line, in the order sent, with every carrier, below.
planThe cheapest way to buy the lines that can be filled, below, or null when none can.
cheapest_itemsEach line's cheapest offer on its own, priced with shipping: the plan's totals without its boxes, or null when no line can be filled.
savingsWhat the plan saves against cheapest_items when both are fully priced, a decimal string, or null.
cheapest_planThe cheapest way with no pins, its totals as in cheapest_items: the same totals as plan when you pin nothing, or null when no line can be filled.
Each of linesMeaning
keyAs sent.
product_idAs sent.
upcThe product's UPC, or null.
titleThe product's title, or null.
quantityAs sent.
destinationThe destination's key, as you sent it.
parcel_destinationWhere its parcel goes: the same key, except for a firearm you sent to an address, which goes on through your store to a dealer not chosen yet. That is your pending destination when you declared one, else a key the plan adds, pending_dealer (or the first free variant), of kind pending with no state.
shipping_classhandgun, long_gun, ammunition or other: which of a distributor's rates applies to it.
in_house_stockUnits you hold yourself, for information: the plan only buys from distributors.
statusplanned: a carrier can fill it. no_carrier: none can. unknown_product: no such product.
carriersEvery selected distributor's offer for it, one per distributor (the offer its plan buys), cheapest first, below.
pin_ignoredtrue when you pinned this line to a distributor that can no longer fill it, so the plan chose another; else false.
Each of carriersMeaning
distributorThe distributor's slug, as GET /distributors lists it.
distributor_nameIts name.
costWhat you pay for one, or null when its feed sent no cost.
map_priceThe minimum advertised price, or null.
stockUnits it has.
drop_shipWhether this offer may be drop-shipped: allowed, blocked or unknown, which counts as allowed.
drop_ship_programWhether the distributor's terms offer drop-ship at all: offered, not_offered or unknown.
routesHow it can reach the destination: direct, via_store or to_store.
refusalWhy it cannot fill the line: short_stock (fewer than the quantity), no_cost or state_restricted (it will not ship it to any state the line could go through). Null when it can.
planned_routeThe route the plan uses it by, or null when the plan does not use it.
logo_urlThe distributor's logo, or null when there is none.
if_chosenWhat the order would cost with it filling the line: its route, the order's total, complete, unknown_shipments and the difference from the plan's total, negative when it costs less. The plan itself for the carrier the plan uses; null when it cannot fill the line.
planMeaning
items_totalCost times quantity, every line the plan fills.
distributor_shippingEvery distributor box's shipping that is published.
store_shippingEvery box you ship on, from FFL Monkey's estimate.
totalAll three.
completetrue when every box has a published rate.
unknown_shipmentsBoxes with no published rate, left out of the totals.
shipmentsThe boxes, below.
Each of shipmentsMeaning
kinddistributor, a distributor's box, or store, your own box on to the destination.
distributorIts slug; null for a store box.
distributor_nameIts name; null for a store box.
routeWhich of the shipping terms priced it: drop_ship, store or any.
ship_todestination or store.
destinationThe destination's key its lines were sent to, as you sent it; null when it goes to your store.
parcel_destinationWhere the box goes: its lines' parcel_destination; null when it goes to your store.
linesThe lines in it, each its key, quantity, unit_cost and line_cost.
subtotalThe lines in it.
shippingIts shipping, or null when the rate is not published.
chargesThe rate and fees that make up shipping, each a label and an amount.
terms_sourceWhere the shipping terms were read, or null.
terms_checked_onWhen they were last checked, or null.
logo_urlThe distributor's logo, or null for a store box.

Returns

  • 200The plan, in data. A plan nobody can fill is still 200, with plan null.
  • 422validation_failed: the body broke a rule, a line naming an undeclared destination or a pin naming a line not sent included.

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

Chapter42

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

42.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.

Chapter43

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

43.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.

Chapter44

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

44.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.

Chapter45

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

45.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.

Chapter46

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

46.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.

Chapter47

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

47.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.

Chapter48

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

48.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 Data Feed API, version 1. Base URL https://backend.fflmonkey.com/api/v1/inventory and https://backend.fflmonkey.com/api/v1. Questions: Support in your FFL Monkey account, https://backend.fflmonkey.com/dashboard/support, or [email protected].