Resolve before you read
Every integration starts with a name somebody typed and has to end with one catalog id. This is how to get there, and what to do when the answer is uncertain.
Which call answers which question
Three endpoints take a string and none of them answers the same question. Picking the wrong one is the single most common reason an integration shows the wrong firearm.
| What you have | What to call | Why |
|---|---|---|
| A name a person wrote, and you need the record it means | GET /v1/firearms/resolve | Answers which record a string is, and reports how certain it is. Deterministic, so the same query returns the same id indefinitely. |
| A search box, and you need a page of plausible results | GET /v1/firearms/search | Ranks records against a query for a human to choose from. It is not a decision, and its first row is not an answer. |
| A filter, a facet or a browse page | GET /v1/firearms | Structured filters (manufacturer, caliber, category, year, action) that mean the same thing every time. Use these before you reach for text. |
The reference for both is on the resolve page and the search page. If a reader is choosing, you want search; if your code is choosing, you want resolve.
Resolve many names at once
Extracting the firearms named in a document, a spreadsheet column or a chat message means dozens of names at once. One request per name turns an inline feature into a background job.
// Fifty names, one round trip. Each result echoes its own query,// so nothing has to be matched up by position.const res = await fetch('https://api.gunspec.io/v1/firearms/resolve', { method: 'POST', headers: { 'X-API-Key': process.env.GUNSPEC_API_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ queries: ['G19 gen 5 MOS', 'AK-47', 'H&K MP5', 'that one from Die Hard'], }),}) const { data } = await res.json()for (const result of data.results) { // An unknown name is a result with a status, not an error: one // bad name in fifty does not fail the batch. if (result.status !== 'resolved') { review.push(result) continue } resolved.set(result.query, result.firearmId)}- Up to fifty queries in one call, and each result echoes its own
query, so nothing has to be matched up by position. - An unknown name comes back as a result with
status: "not_found", never as an error. One bad name does not fail the other forty-nine. - A batch costs one request against your quota rather than fifty, so it is worth building even where a loop would work.
Treat the score as a decision, not a decoration
Each result carries a status and a score, because a name can be unambiguous, plausible or meaningless and those need three different behaviours. Write the rule once and use it everywhere a name arrives.
// One decision, written once, used everywhere a name arrives.function decide(result) { switch (result.status) { case 'resolved': // Certain enough to act on unattended. Below that, the answer // is probably right, which is not the same thing. return result.score >= 0.9 ? { action: 'use', id: result.firearmId } : { action: 'confirm', id: result.firearmId, from: result.query } case 'ambiguous': // Several records fit equally well and the API deliberately // did not choose. Offer `alternatives`; do not pick [0]. return { action: 'choose', options: result.alternatives } default: // Nothing matched. `unresolvedTokens` names the words that // carried no meaning, which is what to show the user. return { action: 'ask', unresolved: result.unresolvedTokens } }}Do not silently fall back to search when resolution fails. A top hit presented as an answer is how the wrong firearm reaches a customer. An ambiguous result already carries its candidates, so show them.
Store the id, not the name
A catalog id is the stable thing in this system. Resolving the same name on every page view spends a request to re-derive something that has not changed since last month.
- Resolve once, store the id beside your own record, and call the catalog by id from then on.
- Store
versiontoo: it changes when and only when a field we serve changes, so it tells you whether your copy is stale without another lookup. Caching and freshness covers the rest. - Keep the original string as well. When a record is merged or renamed, the query that produced the id is what lets you re-resolve without asking the user again.
Four habits worth unlearning
Each of these works in a demo and fails in front of a customer.
| Instead of | Do this |
|---|---|
| Searching and taking the first result | Resolve, and act on the status. Search is for a human to choose from. |
| Guessing an id from a name ("glock-19") | Ask the API. Slugs follow the catalog, not a formatting rule you can reproduce. |
| Resolving the same name on every request | Resolve once, store the id, and cache the mapping as long as you like. |
| Dropping the words that did not match | Read unresolvedTokens. A variant we do not hold appears there rather than being quietly ignored. |