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" } }
- Request from an unregistered address →
403 ip_not_allowed - Key with no registered IPs →
403 ip_whitelist_required - View your registered IPs any time with GET
/v1/account - Allowlist changes take effect within about two minutes.
- Your servers need static egress IPs — serverless/dynamic-IP callers must route API traffic through a NAT gateway or proxy with a fixed address (see how).
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 runs | Recommended 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.
2. Rate limits & usage
Requests are metered per key. When a limit is hit the API returns 429
with a Retry-After header.
| Tier | Burst | Daily quota |
|---|---|---|
| standard | 60 requests/minute | 10,000 requests/day |
| pro | 300 requests/minute | 100,000 requests/day |
| enterprise | 1,000 requests/minute | 1,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
- Schema-validated saves. The document is validated against the
configuration JSON Schema before anything is written. Failures return
400 schema_validation_failedwith a list of{path, message}errors and nothing is saved. Each distributor's fields sit at theconfig_keypaths declared in itsconfig_schema(see GET/v1/distributorsand theDistributorConfig*schemas in the API reference). - Masked secrets round-trip safely. Credential values you read
back as
********can be sent back unchanged — the stored secret is preserved. Only send real values when setting or changing a credential. - Read-modify-write. Always start from a fresh
GET
/v1/config; the PUT replaces the whole document, so pushing a stale copy reverts newer changes. - Add
?validate=trueto also run product-count validation — the save is rejected with409if it would delete more of your live catalog than your configured delete threshold allows. - Every accepted save is recorded in the configuration history for audit and rollback support.
3.3 Validate your configuration
POST /v1/config/validate has two
modes:
- Saved-config validation (empty body): JSON Schema conformance,
unset fields required by each active distributor's field schema, and live
credential validation against each active distributor's API, returning
per-distributor
validflags plus warnings. Takes several seconds per distributor — call it after configuration changes, not routinely. - Dry-run (body
{"configuration": {...}}): schema-validates a candidate document without saving and returns{dry_run, schema_valid, errors}— wire it into your CI or admin UI before pushing.
curl -s -X POST https://api.fflcockpit.com/v1/config/validate -H "x-api-key: YOUR_API_KEY"
4. Product search
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"
/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):
| Parameter | Type | Description |
|---|---|---|
q | string | Free text across name, brand, SKU, MPN; a value of 8+ digits searches UPC exactly. |
upc | string | Exact UPC match. |
sku | string | Exact distributor SKU match. |
distributor | string | Limit to one distributor id
(distid) from your active set. |
manufacturer | string | Exact manufacturer/brand name. |
product_class | string | Exact product class. |
category | string | Exact category name. |
min_price / max_price | number | Dealer-cost bounds (inclusive). |
in_stock | boolean | true = only items with
qty_on_hand > 0. |
drop_ship_only | boolean | true = only
drop-shippable items (drop_ship_flg = 1). |
ffl_required | boolean | true = only
FFL-required items (ffl_req = 1). |
sort | enum | price, -price,
name (default), -name, qty, -qty
(- = descending). |
page | integer | 1-based page number (default 1; max depth 10,000 rows). |
page_size | integer | Results 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
- Poll, compare, download. Re-call each cycle (~every 20 minutes)
and skip downloading when
versionis unchanged. - First call after a new snapshot or a configuration change may take up to ~60 seconds while your filtered export is generated; subsequent calls return instantly.
- URLs are short-lived (default 15 minutes,
expires_inaccepts 60–3600 seconds) — download promptly, never store them. - Columns match the product record everywhere in this API:
identifiers (
distid,distsku,stockid,upc), names and full descriptions,ffl_req,unit_price(custom pricing applied),shipping_cost,total_cost,map_price,msrp,qty_on_hand,drop_ship_flg,images, and category fields. Slice per distributor client-side via thedistidcolumn. include_attributes=truealso presigns the product attributes feed (csv.gz:upc, distid, attribute_name, attribute_value) for building faceted navigation.
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"
| Endpoint | What it returns |
|---|---|
/v1/analytics/top-products | Highest-movement products
(sales proxy, restocks, movement score, cost, newness flags) — scope with
product_class / category_id. |
/v1/analytics/on-sale | Meaningful price drops (reference vs. current price, drop percent). |
/v1/analytics/margin-opportunities | Best cost-to-MAP/MSRP
spreads (sort_by=gross_profit|margin_pct). |
/v1/analytics/trending | Sales acceleration — 7-day
pace vs. the prior 23-day average (trend_ratio). |
/v1/analytics/new-arrivals | Products first seen within the window. |
/v1/analytics/newly-available | Restocks — products back in stock after sitting at zero. |
/v1/analytics/price-gaps | Products one distributor sells well below the peer average. |
7.1 Common parameters
| Parameter | Type | Description |
|---|---|---|
window | enum | Rollup window: 7d
(default), 30d, 60d, 90d. Ignored by
metrics without a window (e.g. margin-opportunities). |
limit | integer | Rows to return (default 50, max 200). |
product_class | string | Comma-separated 2-letter product class codes; rows must match one of them. Full code list in 7.2 below. |
category_id | string | Comma-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 Accessories | AG Air Guns & Accessories |
AO Ammunition | AP Apparel |
AR Archery | BP Black Powder Firearms |
BS Binoculars & Spotting | FA Firearms |
FI Fishing | FP Firearms Parts |
HS Holsters | HT Hunting |
HZ Hazardous | KN Knives |
LL Lights & Lasers | MG Magazines |
MZ Muzzleloading | OP Optics |
OT Other | RC Range Bags & Cases |
RL Reloading | SF 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"
| Parameter | Type | Description |
|---|---|---|
zip | string, required | Customer's 5-digit ZIP. |
radius | integer | Miles, 1–50 (default 15). |
name | string | Optional 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:
items[].best— the cheapest source (unit price × qty + estimated shipping) with up to fivealternatives;plan.orders— the selections grouped into per-distributor orders with product subtotals and shipping estimates, plusplan.total_estimate;unfulfillable— items no active distributor can cover at the requested quantity.
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:
| Field | Required | Description |
|---|---|---|
order_id | yes | Your 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.name | yes | Purchaser's full name. |
customer.email | yes | Purchaser's email. |
customer.phone | no | Recommended — some distributors require a phone number. |
customer.address1 | yes | Street address (drop-ship destination). |
customer.address2 | no | Suite/unit. |
customer.city / state / zip | yes | City, 2-letter state code, ZIP. |
items[] | yes | 1–100 line items. |
items[].distid | yes | Sourcing distributor id —
take from the quote's best or product search. |
items[].distsku | yes | Distributor SKU. |
items[].upc | yes | Item UPC. |
items[].qty | yes | Positive integer. |
items[].ffl_req | no | true marks a
firearm/regulated item that must ship to an FFL; drives routing and order
splitting (default false). |
ffl.licenseNumber | conditional | Destination dealer's
license (from the FFL directory). Required when any item has
ffl_req: true and ship_to_store is false. |
ship_to_store | no | true routes the whole
order to your configured store (default false). |
po_number | no | Passed 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 type | How to request it | Where 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.
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:
- GET
/v1/orders/{order_id}— the distributor orders for one of your order ids. - GET
/v1/orders— simple paged history (limit,offset,sort,direction,search). - GET
/v1/fulfillment/history— the query endpoint: returns all records matching your filters, e.g. look up tracking by PO number or order number.
# 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"
| Parameter | Description |
|---|---|
reference | Matches 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_status | Scope filters (API-placed orders have
order_source: cockpit_cart). |
from / to | Order-date bounds,
YYYY-MM-DD (inclusive). |
limit / offset | Paging (limit ≤ 100,
default 25); count in the response is the total match count. |
include_errors | Default 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:
- Strong signals match like-for-like only (order shipping vs. reported shipping, order billing vs. reported billing) — the actual cardholder victim is never flagged by their own address.
- Names, IPs, and order items never trigger a match alone; they are returned as corroboration on already-matched reports.
- Addresses, emails, and phones are normalized before matching (unit/street abbreviations, gmail dots/plus-tags, phone formatting).
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):
| Fields | Notes |
|---|---|
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, phone | Strong signals; normalized before matching (gmail dots/plus-tags, phone formatting). |
ip_address | Corroboration only. |
order_items | Array of {upc, sku, name, qty, price};
corroboration only (first 50 stored). |
order_source, order_id, order_json,
notes | Report 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.
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
- Server-side only. Your API key is a credential for real financial actions (distributor orders). Call this API from your backend only — never embed the key in browser JavaScript, mobile apps, or public repositories.
- IP allowlisting is mandatory. Keys only work from your
registered server IPs, so a leaked key is useless from anywhere else. Keep the
list current (and minimal) via support; review it with
GET /v1/account. - Rotate on suspicion. If a key may have leaked, contact
support@garidium.com immediately to rotate
it. Watch GET
/v1/usagefor request patterns you don't recognize. - Order placement is independently capped at 60/hour and 500/day
per key (
429 order_volume_limit) so a leaked key cannot mass-order; contact support to raise the cap for legitimate volume. - Least privilege in your stack. Store the key in a secrets manager or environment variable, not in code; scope access to the services that place orders and sync products.
- Authenticated responses are marked
Cache-Control: no-store— don't cache them in shared proxies. Request bodies are capped at 1 MB (413).
14. Errors
Errors use conventional HTTP status codes with a machine-readable body:
{ "success": false, "error": { "code": "rate_limited", "message": "Rate limit exceeded ..." } }
| Status | Meaning |
|---|---|
400 | Invalid parameters or body (invalid_parameter, invalid_json, invalid_upc, ...) |
401 | Missing x-api-key header |
403 | Key not enabled for the Developer API, cancelled, or caller IP not on the key's allowlist |
404 | Unknown endpoint, distributor, or product |
405 | Wrong HTTP method for the endpoint |
409 | Request conflicts with current state
(no_active_distributors, no_validated_distributors,
duplicate_order, validation_failed,
fulfillment_not_configured) |
413 | Request body larger than 1 MB |
429 | Rate limit exceeded — honor Retry-After |
500 | Unexpected server error — safe to retry with backoff |
503 | Dependency 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).