GunSpec

Seller

A shop reaches these endpoints in four steps, and the third is the one nothing else explains.

  1. Hold an Enterprise plan. Listings are a plan perk. Nothing in these tables bills anyone and there is no separate advertising product.
  2. Apply, and be approved by a person. Apply at https://gunspec.io/en/sellers. Approval creates the shop; until then nothing of yours is public.
  3. Name a key. There is no such thing as a "vendor key" and no scope column on keys. You create an ordinary API key, then name it on the shop in Profile > Seller > Shop settings > Pushing from a server. That mapping is the entire vendor scope: the shop says which key it accepts a push from, and your key value is never sent to us. Several shops may name the same key; when they do, every call below needs ?vendor=, and GET /v1/vendor/shops lists the ids that key may act for.
  4. Read, then write. GET /v1/vendor/offers for what we hold, PATCH for a price or stock change, PUT for a bulk upsert, DELETE to withdraw. Read first and a nightly sync sends the rows that changed rather than all of them.

Four rules the API enforces and a retailer usually meets late:

  • Money is integer minor units plus an ISO 4217 code. 139900 with AUD is A$1,399.00. A float is refused at the boundary rather than rounded quietly. Minor units are not always hundredths: JPY has none, KWD has three. Send the units of the currency you name.
  • `stock_qty` is private and never appears in a public response. 0 renders as "No stock at this supplier", any other number as in stock, null as not tracked. A live count is competitive information, so we store it and never publish it. If you do not count units, send in_stock instead: a row that carries a quantity derives the flag from it, so the two cannot disagree.
  • Writing is also a claim that the price still stands. An offer nobody has refreshed in 30 days from a feed (90 from the console) stops appearing, because a stale price is worse than none. Re-pushing an unchanged row is how you say nothing changed.
  • A mapping error is a review, not a rejected batch. SKUs naming an id we do not hold come back in unmatched. A file of 400 rows is not refused because three ids were wrong.

GunSpec never takes the order. An offer carries a URL and links out; there is no basket, payment or customer anywhere in this surface. Public rows link through /v1/out/{clickId}, which counts the visit and redirects to you, so the traffic we send is measurable - read it in Profile > Seller > Traffic. region on an offer hides sellers who do not ship somewhere: a display convenience, never a statement about what is lawful to own.

Authentifizierung erforderlichExplorer+

Who stocks this part and where to buy it. Offers link out; GunSpec never takes the order, holds a basket or sees a payment. Only approved vendors appear, and an offer not refreshed by its seller within 30 days is hidden, because a stale price is worse than none. region hides sellers who do not ship there, which is a display filter and never a statement about what is lawful to own.

GET/v1/attachments/{id}/offers

  • stringHeadererforderlich

    Ihr API-Schlüssel. Authorization: Bearer <key> wird ebenfalls akzeptiert.

  • stringerforderlich

    Attachment slug

    Akzeptiert
    a lowercase slug: letters, digits and hyphens
    Beispiel

  • string

    ISO 3166-1 alpha-2 code; hides sellers who do not ship there

    Beispiel

  • PublicOffer[]

    The response payload.

  • object

    The shop stocking the part.

  • string

    Slug of the shop.

  • string

    Display name, as the maker writes it.

  • string | null

    Official website, where one is known.

  • string | null

    Absolute URL of the logo, or null where none is on file.

  • string

    The shop's own catalogue key for the listing. A seller's identifier, not ours: it is what PATCH /v1/vendor/offers/{sku} addresses.

  • integer

    Price in integer minor units of currency, so cents for AUD. Never a float. Format with Intl.NumberFormat at render rather than dividing first.

  • string

    ISO 4217 code the price is in, e.g. AUD. Prices are integer minor units; format with Intl.NumberFormat at render rather than dividing first.

  • string

    Absolute, directly fetchable URL.

  • boolean

    Whether the shop reports it in stock. Derived from stockQty where the shop tracks a count.

  • string | null

    Opaque token for the outbound link: send readers to /v1/out/{clickId} so the visit is counted for the seller. Null on a listing that cannot currently be followed.

  • string | null

    ISO 3166-1 alpha-2 code the seller ships to. Null means they state no restriction.

  • string

    When the seller last refreshed this listing. A listing not refreshed inside its freshness window is hidden rather than shown stale.

  • 400VALIDATION_ERRORValidation error
  • 401UNAUTHORIZEDAPI key required
  • 403FORBIDDENValid key, not permitted
  • 404NOT_FOUNDResource not found
  • 429RATE_LIMITEDRate limit exceeded
  • 500INTERNAL_ERRORUnexpected server error

Three guards are applied for you: an unapproved shop never appears, a listing not refreshed inside its window is hidden, and a shop whose plan has lapsed goes dark rather than being deleted.

Prices come back as integer minor units. Format with Intl.NumberFormat; never divide before rendering.

Link out through clickId rather than url if you want the shop to see the traffic attributed.

Mindeststufe
Explorer+
Monatskontingent
200/mo
Anfragerate
10/min
Tageslimit
50

Die Werte gelten für die Mindeststufe dieses Endpunkts. Höhere Stufen erhöhen alle Limits.

Authentifizierung erforderlichExplorer+

Who stocks this firearm and where to buy it. The same three guards as an attachment's offers apply: only approved vendors appear, an offer not refreshed within 30 days is hidden, and region hides sellers who do not ship there. Offers link out; GunSpec never takes the order.

GET/v1/firearms/{id}/offers

  • stringHeadererforderlich

    Ihr API-Schlüssel. Authorization: Bearer <key> wird ebenfalls akzeptiert.

  • stringerforderlich

    Firearm slug

    Akzeptiert
    a lowercase slug: letters, digits and hyphens
    Beispiel

  • string

    ISO 3166-1 alpha-2 code; hides sellers who do not ship there

    Beispiel

  • PublicOffer[]

    The response payload.

  • object

    The shop stocking the part.

  • string

    Slug of the shop.

  • string

    Display name, as the maker writes it.

  • string | null

    Official website, where one is known.

  • string | null

    Absolute URL of the logo, or null where none is on file.

  • string

    The shop's own catalogue key for the listing. A seller's identifier, not ours: it is what PATCH /v1/vendor/offers/{sku} addresses.

  • integer

    Price in integer minor units of currency, so cents for AUD. Never a float. Format with Intl.NumberFormat at render rather than dividing first.

  • string

    ISO 4217 code the price is in, e.g. AUD. Prices are integer minor units; format with Intl.NumberFormat at render rather than dividing first.

  • string

    Absolute, directly fetchable URL.

  • boolean

    Whether the shop reports it in stock. Derived from stockQty where the shop tracks a count.

  • string | null

    Opaque token for the outbound link: send readers to /v1/out/{clickId} so the visit is counted for the seller. Null on a listing that cannot currently be followed.

  • string | null

    ISO 3166-1 alpha-2 code the seller ships to. Null means they state no restriction.

  • string

    When the seller last refreshed this listing. A listing not refreshed inside its freshness window is hidden rather than shown stale.

  • 400VALIDATION_ERRORValidation error
  • 401UNAUTHORIZEDAPI key required
  • 403FORBIDDENValid key, not permitted
  • 404NOT_FOUNDResource not found
  • 429RATE_LIMITEDRate limit exceeded
  • 500INTERNAL_ERRORUnexpected server error

Mindeststufe
Explorer+
Monatskontingent
200/mo
Anfragerate
10/min
Tageslimit
50

Die Werte gelten für die Mindeststufe dieses Endpunkts. Höhere Stufen erhöhen alle Limits.

ÖffentlichExplorer+

The outbound link behind every 'Where to buy' row. Redirects to the seller's own product page and counts the visit against that listing, which is how a shop can see what their placement sent them. The destination comes from the offer row and never from the request, so it cannot be used as an open redirect. Bots are not counted, and repeat clicks from one visitor within half an hour count as one visitor rather than one each. A listing that has lapsed, been unpublished or whose shop is suspended redirects to the catalog page for the product instead, and counts nothing. Public, because the readers it serves are anonymous visitors.

GET/v1/out/{clickId}

Keine Authentifizierung. Dieser Endpunkt antwortet ohne Schlüssel.

  • stringerforderlich

    The listing's tracking token, from clickId on an offer

  • string

    Locale for the catalog page a lapsed listing falls back to

    Akzeptiert

  • 404NOT_FOUNDResource not found
  • 429RATE_LIMITEDRate limit exceeded
  • 500INTERNAL_ERRORUnexpected server error

Mindeststufe
Explorer+
Monatskontingent
200/mo
Anfragerate
10/min
Tageslimit
50

Die Werte gelten für die Mindeststufe dieses Endpunkts. Höhere Stufen erhöhen alle Limits.

Authentifizierung erforderlichEnterprise

An integration is handed a key, not a shop id. This is where it learns them. An account may run more than one shop and name the same key on each; when it has, every other vendor call needs ?vendor= to say which, and this endpoint is how a script finds the id rather than having it read out of the website. A shop that is not approved accepts writes but shows nothing publicly.

GET/v1/vendor/shops

  • stringHeadererforderlich

    Ihr API-Schlüssel. Authorization: Bearer <key> wird ebenfalls akzeptiert.

  • object[]

    The response payload.

  • string

    Slug of the shop. This is the value to send as ?vendor=.

  • string

    Display name, as the maker writes it.

  • string | null

    Official website, where one is known.

  • string | null

    ISO 4217 code the price is in, e.g. AUD. Prices are integer minor units; format with Intl.NumberFormat at render rather than dividing first.

  • boolean

    Whether staff have approved the shop. An unapproved shop accepts writes but shows nothing publicly.

  • 400VALIDATION_ERRORValidation error
  • 401UNAUTHORIZEDAPI key required
  • 403FORBIDDENValid key, not permitted
  • 429RATE_LIMITEDRate limit exceeded
  • 500INTERNAL_ERRORUnexpected server error

A shop that is not approved accepts writes but shows nothing publicly. Nothing is lost while a reviewer is looking.

If exactly one shop names your key, every other call may omit vendor=.

Mindeststufe
Enterprise
Monatskontingent
1,000,000/mo
Anfragerate
300/min
Tageslimit
50,000

Die Werte gelten für die Mindeststufe dieses Endpunkts. Höhere Stufen erhöhen alle Limits.

Authentifizierung erforderlichEnterprise

What this shop currently holds, so a sync can diff before it writes and send the twelve rows that changed rather than all four hundred. Each row carries the catalogue record it points at, your price and currency, the link, publication status, which side last wrote it (api or console), when it was last refreshed, its clickId, and the clicks it earned over the last 30 days. stockQty appears here and in no public response: it is private from readers, not from the shop that entered it.

GET/v1/vendor/offers

  • stringHeadererforderlich

    Ihr API-Schlüssel. Authorization: Bearer <key> wird ebenfalls akzeptiert.

  • string

    Which of your shops to act for. Required only when one key is named by more than one shop; GET /v1/vendor/shops lists them

    Max. Länge
    64
    Beispiel
  • string

    Only parts or only firearms

    Akzeptiert
    Max. Länge
    40
  • string

    Only drafts or only published rows

    Akzeptiert
  • string

    Match on your SKU or the product name

    Max. Länge
    120
    Beispiel
  • integerstandard1

    Page number, from 1 to 10,000

    Bereich
    1 – 10000
    Beispiel
  • integerstandard20

    Items per page (max 100)

    Bereich
    1 – 100
    Beispiel

  • object[]

    One page of results

  • string

    The shop's own catalogue key for the listing. A seller's identifier, not ours: it is what PATCH /v1/vendor/offers/{sku} addresses.

  • OfferTargetKind

    What a seller listing is for.

  • string

    Slug of the catalogued record the listing points at, in whichever catalog targetKind names.

  • integer

    Price in integer minor units of currency, so cents for AUD. Never a float. Format with Intl.NumberFormat at render rather than dividing first.

  • string

    ISO 4217 code the price is in, e.g. AUD. Prices are integer minor units; format with Intl.NumberFormat at render rather than dividing first.

  • string

    Absolute, directly fetchable URL.

  • boolean

    Whether the shop reports it in stock. Derived from stockQty where the shop tracks a count.

  • integer | null

    Units you hold: 0 is out of stock, null is not tracked. Private to the shop, and never returned by a public offers endpoint.

  • OfferStatus

    Whether a seller listing is public.

  • string

    Where the record came from: a curated entry, an inference, or an import.

  • integer

    Outbound clicks this listing earned over the last 30 days.

  • string

    When a field we serve last changed.

  • number

    Current page number

  • number

    Items per page

  • number

    Total matching records (Builder and above)

  • 400VALIDATION_ERRORValidation error
  • 401UNAUTHORIZEDAPI key required
  • 403FORBIDDENValid key, not permitted
  • 429RATE_LIMITEDRate limit exceeded
  • 500INTERNAL_ERRORUnexpected server error

stockQty appears here and in no public response: it is private from readers, not from the shop that entered it.

A row hidden publicly for being stale is still your row and is listed here, with the updatedAt that explains why.

clicks is what our outbound link sent you over the last 30 days.

Mindeststufe
Enterprise
Monatskontingent
1,000,000/mo
Anfragerate
300/min
Tageslimit
50,000

Die Werte gelten für die Mindeststufe dieses Endpunkts. Höhere Stufen erhöhen alle Limits.

Authentifizierung erforderlichEnterprise

Upserts up to 500 offers keyed by your own SKU. Each offer names exactly one of attachment_id or firearm_id: a SKU is a part or a firearm, never both. A seller maps their catalog to ours once; SKUs naming an id we do not hold come back in unmatched rather than failing the batch, so a mapping error is a review and not a rejected push. Re-pushing an unchanged offer is how you say the price still stands, which is what keeps it out of the 30-day staleness sweep. Prices are integer minor units with an ISO 4217 currency; a float is refused, and currency may be omitted to use your account default. Minor units are not always hundredths (JPY has none and KWD has three), so send the units of the currency you name. Requires an Enterprise key registered to a vendor.

PUT/v1/vendor/offers

  • stringHeadererforderlich

    Ihr API-Schlüssel. Authorization: Bearer <key> wird ebenfalls akzeptiert.

  • string

    Which of your shops to act for. Required only when one key is named by more than one shop; GET /v1/vendor/shops lists them

    Max. Länge
    64
    Beispiel

  • object[]erforderlich

    The listings to upsert, keyed by your own SKU. Up to 500 per request.

  • object

    The response payload.

  • integer

    Offers accepted and stored.

  • object[]

    Rows naming a record we do not hold. Reported rather than failing the batch, so a mapping error is a review and not a rejected push.

  • string

    The shop's own catalogue key for the listing. A seller's identifier, not ours: it is what PATCH /v1/vendor/offers/{sku} addresses.

  • OfferTargetKind

    What a seller listing is for.

  • string

    The id the row named, which we do not hold.

  • 400VALIDATION_ERRORValidation error
  • 401UNAUTHORIZEDAPI key required
  • 403FORBIDDENValid key, not permitted
  • 413PAYLOAD_TOO_LARGERequest body too large
  • 429RATE_LIMITEDRate limit exceeded
  • 500INTERNAL_ERRORUnexpected server error

Each offer names exactly one of attachment_id or firearm_id. A SKU is a part or a firearm, never both.

currency may be omitted to use the shop default, so a seller pricing everything in one currency sets it once rather than on all 500 rows.

Chunk a larger catalogue. A shop may hold 2,000 listings in total.

Mindeststufe
Enterprise
Monatskontingent
1,000,000/mo
Anfragerate
300/min
Tageslimit
50,000

Die Werte gelten für die Mindeststufe dieses Endpunkts. Höhere Stufen erhöhen alle Limits.

Authentifizierung erforderlichEnterprise

Change part of one listing without resending it. The everyday integration is a nightly price and stock sync, and doing that through the batch upsert means resending the target id, the URL and the currency to change one number, which is how a script that meant to reprice a row silently rewrites its link. Every field is optional and absent means unchanged. stock_qty is the private count: 0 is out of stock, a positive number is in stock, and null is "not tracked". in_stock is the flag for a shop that does not count units; send one or the other, never both, since a row that carries a quantity derives the flag from it. Editing a row is also a statement that its price still stands, so it resets the staleness clock.

PATCH/v1/vendor/offers/{sku}

  • stringHeadererforderlich

    Ihr API-Schlüssel. Authorization: Bearer <key> wird ebenfalls akzeptiert.

  • stringerforderlich

    Your own SKU for the offer

    Beispiel

  • string

    Which of your shops to act for. Required only when one key is named by more than one shop; GET /v1/vendor/shops lists them

    Max. Länge
    64
    Beispiel

  • integer

    New price in integer minor units of currency. A float is refused.

  • string

    ISO 4217 code the price is in, e.g. AUD. Prices are integer minor units; format with Intl.NumberFormat at render rather than dividing first.

  • string

    Product page on your shop. Must be a full https:// address; any other scheme is refused.

  • integer

    Units you hold: 0 is out of stock, null is not tracked. Send this or in_stock, never both: a row carrying a quantity derives its flag from it.

  • boolean

    Stock flag, for a shop that does not count units. Send this or stock_qty, never both.

  • string | null

    ISO 3166-1 alpha-2 code you ship to. Null states no restriction.

  • string

    Set published to show the listing on public pages, or draft to take it off them while keeping it.

  • object

    The response payload.

  • string

    The shop's own catalogue key for the listing. A seller's identifier, not ours: it is what PATCH /v1/vendor/offers/{sku} addresses.

  • 400VALIDATION_ERRORValidation error
  • 401UNAUTHORIZEDAPI key required
  • 403FORBIDDENValid key, not permitted
  • 404NOT_FOUNDResource not found
  • 413PAYLOAD_TOO_LARGERequest body too large
  • 429RATE_LIMITEDRate limit exceeded
  • 500INTERNAL_ERRORUnexpected server error

Send stock_qty (the private count) or in_stock (the flag), never both.

stock_qty: 0 is out of stock; null is "not tracked" and is not the same thing as zero.

A successful patch resets the staleness clock, because editing a row is also a statement that its price still stands.

Mindeststufe
Enterprise
Monatskontingent
1,000,000/mo
Anfragerate
300/min
Tageslimit
50,000

Die Werte gelten für die Mindeststufe dieses Endpunkts. Höhere Stufen erhöhen alle Limits.

Authentifizierung erforderlichEnterprise

Removes the offer with this SKU. Idempotent: withdrawing what is already gone succeeds.

DELETE/v1/vendor/offers/{sku}

  • stringHeadererforderlich

    Ihr API-Schlüssel. Authorization: Bearer <key> wird ebenfalls akzeptiert.

  • stringerforderlich

    Your own SKU for the offer

    Beispiel

  • string

    Which of your shops to act for. Required only when one key is named by more than one shop; GET /v1/vendor/shops lists them

    Max. Länge
    64
    Beispiel

  • object

    The response payload.

  • boolean

    Always true: the SKU is not listed once this returns, whether or not it was before.

  • 400VALIDATION_ERRORValidation error
  • 401UNAUTHORIZEDAPI key required
  • 403FORBIDDENValid key, not permitted
  • 429RATE_LIMITEDRate limit exceeded
  • 500INTERNAL_ERRORUnexpected server error

To stop showing a listing without losing its history, patch status to draft instead. Its click history survives either way, since traffic is recorded against the SKU.

Mindeststufe
Enterprise
Monatskontingent
1,000,000/mo
Anfragerate
300/min
Tageslimit
50,000

Die Werte gelten für die Mindeststufe dieses Endpunkts. Höhere Stufen erhöhen alle Limits.