API Developer Guide

Build your own storefront or integration on top of the FFL Cockpit distributor network: configure distributors, search and synchronize product data at scale, enforce product/geography restrictions, and protect your checkout with the crowdsourced fraud database. Interactive reference: Swagger UI · machine-readable spec: openapi.json · AI coding agents: point Claude Code / Codex at llms.txt — a single markdown reference with exact contracts, workflows, and invariants built for agents.

1. Getting started & authentication

Every request (except the documentation endpoints) must include your API key in the x-api-key header. Keys are issued with your Developer API subscription — contact support@garidium.com to enroll. All requests use HTTPS and JSON.

# Directly from a whitelisted server
curl -s https://api.fflcockpit.com/v1/ping \
  -H "x-api-key: YOUR_API_KEY"

# Or through your static-egress proxy (see 1.1) - the proxy's egress IP is
# what must be whitelisted; CONNECT tunneling keeps your key off the proxy
curl -s https://api.fflcockpit.com/v1/ping \
  -H "x-api-key: YOUR_API_KEY" \
  -x "http://PROXY_USER:PROXY_PASS@your-proxy-host:3128"

A successful response is always wrapped in the same envelope:

{ "success": true, "data": { "pong": true, "key_name": "My Store", "tier": "standard" } }
All requests must come from a whitelisted IP address. Every API key is locked to a registered list of server IPs — there are no exceptions, and a key with no registered IPs will not authenticate at all. Provide your server IP address(es) when you enroll, and send changes to support@garidium.com.

1.1 No static IP? How to get one

If your application runs on infrastructure without a fixed outbound IP (serverless functions, autoscaling containers, most PaaS platforms), route just your FFL Cockpit API traffic through something that has one. Common setups:

Where your app runsRecommended approach
VPS / dedicated server / EC2, Droplet, Linode… You already have a static IP — whitelist it. (On AWS, attach an Elastic IP so it survives instance changes.)
AWS Lambda / Fargate / autoscaling EC2 Place the workload in a VPC and route egress through a NAT Gateway with an Elastic IP — every function/container then exits from that one IP. (GCP: Cloud NAT; Azure: NAT Gateway.)
Heroku / Render / Railway / Fly.io Use the platform's static-outbound-IP feature or a static-IP add-on (e.g. QuotaGuard Static, Fixie), which is a hosted forward proxy — the add-on gives you the IPs to whitelist.
Vercel / Netlify / Cloudflare Workers No static egress on standard plans — run a small forward proxy (below) or move the API-calling code to a backend that has one.
Local development Have your office/home IP added to the allowlist, or tunnel through your proxy/VPN below.

Option A — forward proxy on a $6 VPS (works from anywhere)

Rent the smallest VPS at any provider (its IP is static — whitelist it), install a proxy such as Squid, and send only your API calls through it:

# On the VPS (Ubuntu): install and lock down Squid
sudo apt install squid apache2-utils
sudo htpasswd -c /etc/squid/passwords apiproxy         # set a strong password

# /etc/squid/squid.conf - allow ONLY authenticated CONNECT to the API host
auth_param basic program /usr/lib/squid/basic_ncsa_auth /etc/squid/passwords
acl authed proxy_auth REQUIRED
acl api_host ssl::server_name api.fflcockpit.com
http_access allow authed api_host
http_access deny all
http_port 3128
sudo systemctl restart squid
# Your application: route API traffic via the proxy
# Python (requests honors proxy settings)
requests.get("https://api.fflcockpit.com/v1/ping",
             headers={"x-api-key": KEY},
             proxies={"https": "http://apiproxy:PASSWORD@YOUR-VPS-IP:3128"})

# Node 18+ (undici)
import { ProxyAgent } from "undici";
await fetch("https://api.fflcockpit.com/v1/ping", {
  headers: { "x-api-key": KEY },
  dispatcher: new ProxyAgent("http://apiproxy:PASSWORD@YOUR-VPS-IP:3128") });

# Or process-wide for tools that honor it (curl, requests, many SDKs)
export HTTPS_PROXY=http://apiproxy:PASSWORD@YOUR-VPS-IP:3128

Because the proxy speaks CONNECT, your traffic stays end-to-end TLS — the VPS never sees your API key. Firewall port 3128 to your own infrastructure's address ranges for extra safety.

Option B — VPN/WireGuard tunnel

If you already run a VPN (WireGuard, Tailscale exit node, OpenVPN) on a static-IP host, route api.fflcockpit.com traffic through it and whitelist the exit IP.

Whitelist as few IPs as possible — one NAT/proxy IP is better than a range. If you change hosting, email the new IP to support@garidium.com before cutover to avoid downtime (changes take effect within ~2 minutes).

2. Rate limits & usage

Requests are metered per key. When a limit is hit the API returns 429 with a Retry-After header.

TierBurstDaily quota
standard60 requests/minute10,000 requests/day
pro300 requests/minute100,000 requests/day
enterprise1,000 requests/minute1,000,000 requests/day

Because bulk product data is delivered as S3 downloads (section 5), a full catalog sync costs a handful of API requests regardless of catalog size — the standard tier comfortably supports syncing every 20 minutes.

Track your consumption with GET /v1/usage (today, this month, per-endpoint and per-day rollups) and your tier/limits with GET /v1/account.

3. Configuration management

Product data is scoped to the distributors you have configured and marked active. The typical flow:

3.1 Discover supported distributors

curl -s https://api.fflcockpit.com/v1/distributors -H "x-api-key: YOUR_API_KEY"

Each entry includes the distributor's distid, whether automated fulfillment is supported, a config_schema describing the fields that distributor needs (account numbers, FTP/API credentials, etc.), and a default_config template.

3.2 Manage your configuration document

Configuration is managed en masse, the same way the FFL Cockpit WordPress plugin does it: one JSON document holds everything (distributors, targets, fulfillment, pricing, product restrictions), and you push the whole document on every change.

# 1. Fetch the current document (credential values arrive masked with '*')
curl -s https://api.fflcockpit.com/v1/config -H "x-api-key: YOUR_API_KEY" > config.json

# 2. Fetch the JSON Schema it must satisfy (draft-04; feed it to any validator)
curl -s https://api.fflcockpit.com/v1/config/schema -H "x-api-key: YOUR_API_KEY" > schema.json

# 3. Edit config.json - e.g. enable AmChar under the distributors section:
#    "distributors": { "AmChar": { "distid": "AMC", "active": true,
#                                  "fulfillment": { "api_key": "your-dealer-api-key" } } }

# 4. Push the FULL document back
curl -s -X PUT https://api.fflcockpit.com/v1/config   -H "x-api-key: YOUR_API_KEY" -H "content-type: application/json"   -d @config.json

3.3 Validate your configuration

POST /v1/config/validate has two modes:

curl -s -X POST https://api.fflcockpit.com/v1/config/validate -H "x-api-key: YOUR_API_KEY"

GET /v1/products/search queries the unified inventory snapshot (refreshed continuously from your active distributors). Intended for interactive lookups — it is offset-paged (max 100 per page) and depth-capped. Use the catalog feed for full pulls.

curl -s "https://api.fflcockpit.com/v1/products/search?q=glock%2019&in_stock=true&sort=price&page_size=25" \
  -H "x-api-key: YOUR_API_KEY"
What you search is your catalog. All product endpoints read the S3 catalog feed for your active distributors with your product restrictions, cost filters, and custom catalog pricing already applied — the same data your website sync would list. Products only surface for distributors that have passed credential validation (run POST /v1/config/validate after setting credentials; until then product endpoints return 409 no_validated_distributors). Pass ignore_restrictions=true on any product endpoint to skip your product restrictions and cost filters (custom pricing and the validation gate still apply). Data refreshes about every 20 minutes.

Complete parameter reference (all optional, combinable):

ParameterTypeDescription
qstringFree text across name, brand, SKU, MPN; a value of 8+ digits searches UPC exactly.
upcstringExact UPC match.
skustringExact distributor SKU match.
distributorstringLimit to one distributor id (distid) from your active set.
manufacturerstringExact manufacturer/brand name.
product_classstringExact product class.
categorystringExact category name.
min_price / max_pricenumberDealer-cost bounds (inclusive).
in_stockbooleantrue = only items with qty_on_hand > 0.
drop_ship_onlybooleantrue = only drop-shippable items (drop_ship_flg = 1).
ffl_requiredbooleantrue = only FFL-required items (ffl_req = 1).
sortenumprice, -price, name (default), -name, qty, -qty (- = descending).
pageinteger1-based page number (default 1; max depth 10,000 rows).
page_sizeintegerResults per page (default 50, max 100).

GET /v1/products/facets returns the sorted distinct manufacturers, categories, and product classes across your restricted catalog — use it to build search filters and faceted navigation (cache client-side, refresh daily). The values feed the matching search parameters above.

5. Bulk catalog sync (S3)

Bulk product data is delivered through S3, not through the REST API. GET /v1/products/catalog returns a presigned URL to a single Parquet file containing your complete restricted catalog — your active distributors, with your product restrictions, cost filters, and custom catalog pricing applied. It is exactly the data search and detail serve, in bulk:

{
  "success": true,
  "data": {
    "version": "v=20260719T150642",
    "format": "parquet", "compression": "snappy",
    "rows": 66109,
    "restrictions_applied": true,
    "custom_pricing_applied": true,
    "expires_in_seconds": 900,
    "url": "https://garidium.s3.amazonaws.com/feeds/api-exports/..."
  }
}

A complete sync loop in Python:

import pandas as pd, requests

API = "https://api.fflcockpit.com/v1"
HEADERS = {"x-api-key": "YOUR_API_KEY"}
last_version = None

feed = requests.get(f"{API}/products/catalog", headers=HEADERS).json()["data"]
if feed["version"] != last_version:            # new snapshot published?
    catalog = pd.read_parquet(feed["url"])     # download NOW; the URL expires
    last_version = feed["version"]
    # upsert `catalog` into your database keyed on (distid, stockid);
    # diff against your stored copy for price/quantity deltas
Compliance reminder. Prices are your distributor costs. When you publish products on your own site, respect each manufacturer's MAP policy (map_price) and check restrictions before selling into regulated jurisdictions.

6. Product detail

GET /v1/products/{upc} returns every offer for a UPC across your restricted catalog — the full record including descriptions, MAP price, distributor category, and ffl_req (feed it straight into items[].ffl_req at order placement) — so you can pick the best source at order time:

curl -s https://api.fflcockpit.com/v1/products/736676116351 -H "x-api-key: YOUR_API_KEY"

7. Product analytics

Market intelligence over your distributors' catalogs, computed daily from movement history and served one metric per call:

curl -s "https://api.fflcockpit.com/v1/analytics/trending?window=7d&limit=25"   -H "x-api-key: YOUR_API_KEY"
EndpointWhat it returns
/v1/analytics/top-productsHighest-movement products (sales proxy, restocks, movement score, cost, newness flags) — scope with product_class / category_id.
/v1/analytics/on-saleMeaningful price drops (reference vs. current price, drop percent).
/v1/analytics/margin-opportunitiesBest cost-to-MAP/MSRP spreads (sort_by=gross_profit|margin_pct).
/v1/analytics/trendingSales acceleration — 7-day pace vs. the prior 23-day average (trend_ratio).
/v1/analytics/new-arrivalsProducts first seen within the window.
/v1/analytics/newly-availableRestocks — products back in stock after sitting at zero.
/v1/analytics/price-gapsProducts one distributor sells well below the peer average.

7.1 Common parameters

ParameterTypeDescription
windowenumRollup window: 7d (default), 30d, 60d, 90d. Ignored by metrics without a window (e.g. margin-opportunities).
limitintegerRows to return (default 50, max 200).
product_classstringComma-separated 2-letter product class codes; rows must match one of them. Full code list in 7.2 below.
category_idstringComma-separated numeric category ids; rows must match one of them. How to find ids: every analytics row returns its category_id and category_name, and product data (search/detail/catalog export) carries the same taxonomy as item_gb_cat (id) / item_gb_cat_name (name) — so you can go from any product or analytics row to a category filter.

7.2 Product class codes

AC AccessoriesAG Air Guns & Accessories AO AmmunitionAP Apparel
AR ArcheryBP Black Powder Firearms BS Binoculars & SpottingFA Firearms
FI FishingFP Firearms Parts HS HolstersHT Hunting
HZ HazardousKN Knives LL Lights & LasersMG Magazines
MZ MuzzleloadingOP Optics OT OtherRC Range Bags & Cases
RL ReloadingSF Safes SO SOT

The same codes appear on every product record (product_class / product_class_description) and in GET /v1/products/facets.

7.3 Filtering examples

# Trending FIREARMS only, last 7 days
curl -s "https://api.fflcockpit.com/v1/analytics/trending?product_class=FA&window=7d"   -H "x-api-key: YOUR_API_KEY"

# Top movers in ammunition AND magazines over 30 days
curl -s "https://api.fflcockpit.com/v1/analytics/top-products?product_class=AO,MG&window=30d&limit=100"   -H "x-api-key: YOUR_API_KEY"

# Margin opportunities within specific categories (e.g. 981 = Rifle Magazines,
# 982 = Gun Parts - ids from any analytics row or item_gb_cat in product data)
curl -s "https://api.fflcockpit.com/v1/analytics/margin-opportunities?category_id=981,982&sort_by=margin_pct"   -H "x-api-key: YOUR_API_KEY"

# Combine both: on-sale optics in a single category, 60-day reference window
curl -s "https://api.fflcockpit.com/v1/analytics/on-sale?product_class=OP&category_id=3062&window=60d"   -H "x-api-key: YOUR_API_KEY"

Every response has the same envelope: { metric, as_of, window_days, count, results }. Results are scoped to your active distributors. Data refreshes once per day and is served from a shared cache — poll a few times per day at most, not per page view.

8. Product restrictions

POST /v1/restrictions/check evaluates order items against the centrally-maintained restriction rules (state / county / city / ZIP scoping, product scoping by UPC, SKU, class, category, and attributes) plus any overrides configured for your subscription. Call it at checkout before accepting an order:

curl -s -X POST https://api.fflcockpit.com/v1/restrictions/check \
  -H "x-api-key: YOUR_API_KEY" -H "content-type: application/json" \
  -d '{
        "shipping": { "state": "CA", "zip": "94105" },
        "items": [ { "upc": "736676116351" } ]
      }'

The response marks the order approved or not, and each item carries its blocking_rules, warnings, and manual_review matches with customer-facing messages.

9. FFL dealer directory

When a cart contains a firearm (an ffl_req item), the customer must choose a licensed dealer (FFL) to receive it. The directory is synced daily from the ATF FFL database and enriched with FFL Cockpit network data:

9.1 Search near the customer — GET /v1/ffls

curl -s "https://api.fflcockpit.com/v1/ffls?zip=37201&radius=15" -H "x-api-key: YOUR_API_KEY"
ParameterTypeDescription
zipstring, requiredCustomer's 5-digit ZIP.
radiusintegerMiles, 1–50 (default 15).
namestringOptional substring filter on business/license name.

Returns up to 150 dealers, ordered best-first: dealers whose license is already on file with FFL Cockpit in the customer's ZIP, on-file dealers nearby, then the remaining local/nearby dealers. Each record includes the license number, display name, premises address, phone, email, coordinates, ffl_on_file, and sot_on_file (NFA-capable).

9.2 Validate the selection — GET /v1/ffls/{license_number}

Returns the full record for one license — adding transfer fees (transfer_fee_longgun / transfer_fee_handgun), residential (home-based FFL), and contact_required (call before shipping). Masked license segments are supported with X (e.g. 1-23-XXX-XX-XX-12345). Validate the customer's choice here, then pass the license number to order placement as ffl.licenseNumber.

10. Fulfillment quotes (cost optimization)

POST /v1/orders/quote answers "what is the cheapest way to fulfill this order?" For each item (UPC + quantity) it evaluates every active distributor with sufficient stock, applying your configuration — cost adjustments, custom shipping model, product restrictions — and returns:

curl -s -X POST https://api.fflcockpit.com/v1/orders/quote \
  -H "x-api-key: YOUR_API_KEY" -H "content-type: application/json" \
  -d '{ "items": [ { "upc": "764503037276", "qty": 1 },
                   { "upc": "022188882551", "qty": 2 } ] }'

Feed the winning distid/distsku pairs directly into order placement. Shipping estimates are per-item models summed per distributor; consolidated shipments may cost less in practice.

11. Placing & tracking orders

11.1 Place an order — POST /v1/orders

Places real distributor orders for an order captured on your site (you have already collected payment). Each item names its source (distid + distsku + upc + qty):

curl -s -X POST https://api.fflcockpit.com/v1/orders \
  -H "x-api-key: YOUR_API_KEY" -H "content-type: application/json" \
  -d '{
        "order_id": "web-10021",
        "customer": { "name": "Jane Sample", "email": "jane@example.com",
                      "phone": "555-555-0100", "address1": "1 Main St",
                      "city": "Nashville", "state": "TN", "zip": "37201" },
        "items": [ { "distid": "12", "distsku": "GLOCK19GEN5",
                     "upc": "764503037276", "qty": 1, "ffl_req": true } ],
        "ffl": { "licenseNumber": "1-23-456-78-9A-12345" }
      }'

Complete field reference:

FieldRequiredDescription
order_idyesYour unique order reference (idempotency key): 1–50 chars, letters/digits/dash/underscore. Resubmitting an order_id that already produced distributor orders returns 409 duplicate_order instead of double-ordering.
customer.nameyesPurchaser's full name.
customer.emailyesPurchaser's email.
customer.phonenoRecommended — some distributors require a phone number.
customer.address1yesStreet address (drop-ship destination).
customer.address2noSuite/unit.
customer.city / state / zipyes City, 2-letter state code, ZIP.
items[]yes1–100 line items.
items[].distidyesSourcing distributor id — take from the quote's best or product search.
items[].distskuyesDistributor SKU.
items[].upcyesItem UPC.
items[].qtyyesPositive integer.
items[].ffl_reqnotrue marks a firearm/regulated item that must ship to an FFL; drives routing and order splitting (default false).
ffl.licenseNumberconditionalDestination dealer's license (from the FFL directory). Required when any item has ffl_req: true and ship_to_store is false.
ship_to_storenotrue routes the whole order to your configured store (default false).
po_numbernoPassed through to the distributor order. When omitted, one is generated per distributor order as {order_id}-{key prefix/suffix}-{flags} (flags: FFL-required, manual = 1 for API orders, ship-to-store) — e.g. web-10021-ALOM-110 — so a split cart's halves get distinct POs. When supplied, your value is applied to every distributor order in the split. The fulfillment record's po_number is the source of truth.

11.2 Order types & automatic splitting

Order typeHow to request itWhere items ship
Drop-ship to customer ship_to_store: false, item ffl_req: false Directly to customer's address. The chosen product must be drop-shippable (drop_ship_flg: 1 in product data).
FFL transfer item ffl_req: true + ffl.licenseNumber To the licensed dealer; the customer completes the transfer (ID/background check) at the dealer.
Ship-to-store (in-store pickup) ship_to_store: true Everything to your store; FFL items use the store FFL configured at fulfillment.ship_to_store.ffl (ffl may be omitted).

Mixed carts split automatically: the engine creates one distributor order per (distributor, FFL-required-or-not) group. A Glock (ffl_req: true) plus an optic (ffl_req: false) sourced from the same distributor becomes two distributor orders — one to the FFL, one to the customer. With ship_to_store, items group per distributor only. The response's fulfillment_orders array shows the exact split, with one record per distributor order.

Placement is real. A successful call transmits purchase orders to distributors — there is no sandbox. Test against inexpensive accessories first.

11.3 Track orders & fulfillment history

Every order placed through the API is recorded in your fulfillment history when it is created, and the order monitors update each record in place with shipping data (carrier, ship date, tracking number, tracking URL, ship status) as distributors ship. Three ways to read it:

# Tracking info for a PO
curl -s "https://api.fflcockpit.com/v1/fulfillment/history?po_number=PICKUP-10024" -H "x-api-key: YOUR_API_KEY"

# Anything matching a reference (order id, PO, distributor order id, or tracking number)
curl -s "https://api.fflcockpit.com/v1/fulfillment/history?reference=web-10021" -H "x-api-key: YOUR_API_KEY"
ParameterDescription
referenceMatches order_id OR po_number OR distributor_order_id OR tracking number.
order_id, po_number, distributor_order_id, tracking_number Exact-match filters on the corresponding record fields.
distid, order_source, order_status, ship_statusScope filters (API-placed orders have order_source: cockpit_cart).
from / toOrder-date bounds, YYYY-MM-DD (inclusive).
limit / offsetPaging (limit ≤ 100, default 25); count in the response is the total match count.
include_errorsDefault true: failed placement attempts (with failure_reason) are returned in a separate errors array, so an order that never produced a distributor order is still traceable.

Poll on your fulfillment dashboard cadence (e.g. every 15–30 minutes), not per page view.

12. Fraud tools

The crowdsourced fraud database aggregates card-not-present fraud reports from the entire FFL Cockpit dealer network. Three endpoints expose it:

12.1 Check an order — POST /v1/fraud/check

Send the order's identity signals; supply at least one strong signal (shipping_address_1, billing_address_1, email, or phone). Matching is deliberately conservative:

curl -s -X POST https://api.fflcockpit.com/v1/fraud/check \
  -H "x-api-key: YOUR_API_KEY" -H "content-type: application/json" \
  -d '{
        "shipping_name": "John Doe",
        "shipping_address_1": "123 Main St Apt 4",
        "shipping_postcode": "30301",
        "email": "suspect@example.com"
      }'

Complete signal fields (all optional unless noted; shared by check, report, and remove):

FieldsNotes
shipping_name, shipping_address_1, shipping_address_2, shipping_city, shipping_state, shipping_postcode, shipping_country shipping_address_1 is a strong signal (and required for reports).
shipping_is_ffl (boolean) Set true when the order ships to an FFL dealer. FFL shipments are never matched on shipping address and reports of them are rejected.
billing_name, billing_address_1, billing_address_2, billing_city, billing_state, billing_postcode, billing_country billing_address_1 is a strong signal (the stolen cardholder's identity).
email, phoneStrong signals; normalized before matching (gmail dots/plus-tags, phone formatting).
ip_addressCorroboration only.
order_itemsArray of {upc, sku, name, qty, price}; corroboration only (first 50 stored).
order_source, order_id, order_json, notesReport endpoint only: your order reference, the raw order payload (stored for future scoring), and free-text notes shown to other dealers on a match.

A match returns matched: true, the signals in matched_on, report_count (how many distinct dealers reported the identity), aliases used, and reporter notes. Treat a match as a signal to hold the order for manual review, not as an automatic cancellation.

12.2 Report fraud — POST /v1/fraud/reports

When you confirm an order was fraudulent (chargeback, stolen card), report it to protect the network. shipping_address_1 is required; include everything you have — billing identity, email, phone, IP, items, and the raw order JSON.

Never report FFL-destination orders. Orders shipped to an FFL dealer are rejected (shipping_is_ffl) — recording a licensed dealer's premises would poison the shared database for every member.

12.3 Remove your report — DELETE /v1/fraud/reports

Removes only your report matching the supplied signals (e.g. after a false alarm); other dealers' reports are untouched.

Caps per key: 1,000 checks / 25 reports / 25 removals per rolling 24 hours, shared with your FFL Cockpit usage if you also run Cockpit.

13. Security best practices

14. Errors

Errors use conventional HTTP status codes with a machine-readable body:

{ "success": false, "error": { "code": "rate_limited", "message": "Rate limit exceeded ..." } }
StatusMeaning
400Invalid parameters or body (invalid_parameter, invalid_json, invalid_upc, ...)
401Missing x-api-key header
403Key not enabled for the Developer API, cancelled, or caller IP not on the key's allowlist
404Unknown endpoint, distributor, or product
405Wrong HTTP method for the endpoint
409Request conflicts with current state (no_active_distributors, no_validated_distributors, duplicate_order, validation_failed, fulfillment_not_configured)
413Request body larger than 1 MB
429Rate limit exceeded — honor Retry-After
500Unexpected server error — safe to retry with backoff
503Dependency temporarily unavailable (auth store, catalog feed) — retry shortly

Responses larger than ~50 KB are gzip-compressed when your client sends Accept-Encoding: gzip (standard HTTP clients handle this transparently).