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.

Auth RequiredEnterprise

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

  • stringheaderrequired

    Your API key. Authorization: Bearer <key> is accepted in its place.

  • 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 length
    64
    Example
  • string

    Only parts or only firearms

    Accepts
    Max length
    40
  • string

    Only drafts or only published rows

    Accepts
  • string

    Match on your SKU or the product name

    Max length
    120
    Example
  • integerdefault1

    Page number, from 1 to 10,000

    Range
    1 – 10000
    Example
  • integerdefault20

    Items per page (max 100)

    Range
    1 – 100
    Example

  • 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.

Minimum tier
Enterprise
Monthly quota
1,000,000/mo
Rate limit
300/min
Daily cap
50,000

Figures shown are for this endpoint's minimum tier. Higher tiers raise every limit.