What fits what
Compatibility is computed from mount interfaces, never from names. That is why the answers hold up, and why an integration built on names does not.
The model, in four sentences
Understanding this is the difference between reading the endpoints and trusting them. The compatibility reference documents the calls; this is what they mean.
- A firearm exposes interfaces at named positions (a muzzle thread, a rail, a stock mount, a magazine well) from a fixed vocabulary of kind-prefixed ids such as
thread:1/2x28ormag:stanag. - A firearm inherits: its own rows win, then its parent model’s, then the platforms it belongs to. One row on a pattern reaches every rifle built on it.
- An attachment declares what it requires (any-of within a group, all-of across groups; nothing required means universal) and what it provides once fitted. That is how adapters, rail sections and optic plates extend a firearm.
- A fit is therefore computed, and it carries a confidence and a source. Nothing is matched by product name, and a part that says "fits AR-15" in its marketing copy says nothing to this engine.
The order to call them in
Positions first, parts second. A UI built the other way round has to invent its own idea of what slots exist.
- Ask what the firearm exposesThe interfaces endpoint returns the positions and the standards at them, which is the shape of your slot UI.
- Ask what fits, per categoryOptics, muzzle devices, magazines and stocks are different shopping decisions; fetch them as the reader opens each.
- Filter by confidenceSet a minimum so inferred rows do not present as verified ones, and say in the UI what the rest are.
- Attach the offersAsk for offers with the parts when the page is a buying page, rather than one request per row.
// What goes on this rifle, by mount position.// Ask for the positions first when you are building a UI: the// answer is a list of what the firearm actually exposes, so the// slots you render come from the record rather than from a guess.const { data: interfaces } = await gunspec.firearms.getInterfaces('ar-15') // Then the parts, filtered to the ones worth showing. A fit// computed through an inferred row is capped at that row's// confidence, so a minimum drops the weak evidence.const { data } = await gunspec.firearms.getAttachments('ar-15', { category: 'optic', min_confidence: 0.7,})What a confidence score means here
It is evidence about the fit, not a probability that the part is good. Data quality reports the catalog side; these are the rules that matter at the call site.
- A fit computed through an inferred interface is capped at that row’s confidence, so the weakest link sets the number.
- The weakest rows are convention: a rifle tagged threaded with no standard named gets the industry-typical thread for its cartridge or pattern, around 0.5 to 0.6. Never present one as verified.
min_confidencehides them entirely. Pick a floor for your product and apply it consistently: a mixed list with no marking is worse than a short list.- Every row carries a source. When you show a fit, be able to say where it came from.
What is never bridged
Adapters extend what fits. These four are not adapter territory, because being wrong about them is a safety fact rather than a shopping inconvenience.
| Gate | Rule |
|---|---|
| Caliber and bore | Enforced independently of every interface row. A suppressor rated below the cartridge never returns as a fit, whatever the thread says. |
| Threads | Never bridged by an adapter in the engine. A thread pitch either matches or it does not. |
| Magazine wells | Likewise. A well is a well; a conversion is its own catalogued part with its own rows. |
| Minimum barrel length | A rating that requires a minimum barrel is checked against the firearm, not assumed. |
The other direction, and the shop
A part page needs what it fits and where to buy it. Sellers are public on the same terms as the records, and the seller reference covers the shop side. Three filters are applied for you: an unapproved vendor never appears, a listing not refreshed in thirty days is hidden rather than shown stale, and region hides sellers who do not ship there. That last one is a display convenience, never a statement about what is lawful to own.
// The other direction: this part is in the basket, what takes it.const { data } = await gunspec.attachments.getFirearms('surefire-warcomp-556') // And the shop side of the same page. Sellers who do not ship to// this reader are hidden, and a listing nobody has refreshed in// thirty days is hidden rather than shown stale.const { data: offers } = await gunspec.attachments.getOffers('surefire-warcomp-556', { region: 'US' })What your plan can call
Browsing the attachment catalog is open on the same terms as the firearm catalog. Computing fit is Studio: the firearm interfaces and attachments endpoints, the reverse lookups, the platforms, and fits= on the attachment list, which is the same computation wearing a query parameter. Endpoint access by tier lists every gate.