---
title: "Push a vendor's offers"
method: PUT
path: "/v1/vendor/offers"
tags: ["Seller"]
---

# Push a vendor's offers

`PUT /v1/vendor/offers`

## Request

- Base URL: `https://api.gunspec.io`
- URL: `https://api.gunspec.io/v1/vendor/offers`
- Auth: one of:
  - API key in header `X-API-Key`
  - HTTP bearer

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.

**Requires Enterprise tier or higher.**

## Query parameters

- `vendor` string

## Request body

- object
  - `offers` object[], required: The listings to upsert, keyed by your own SKU. Up to 500 per request.
    - `sku` 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.
    - `attachment_id` string: Slug of the catalogued attachment this SKU sells. Send this or `firearm_id`, never both.
    - `firearm_id` string: Slug of the catalogued firearm this SKU sells. Send this or `attachment_id`, never both.
    - `price_cents` integer: Price in integer minor units of `currency`: cents for AUD, none for JPY, thousandths for KWD. A float is refused.
    - `currency` 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.
    - `url` string: Product page on your shop. Must be a full https:// address; any other scheme is refused.
    - `in_stock` boolean: Whether you currently stock it.
    - `region` string, nullable: ISO 3166-1 alpha-2 code you ship to. Null states no restriction. A display filter for readers, never a statement about what is lawful to own.

## Response `200`

Push a vendor's offers

- object
  - `success` boolean, required: Always true on a successful response. Every error carries `false` and an `error` object instead.
  - `data` object, required: The response payload.
    - `written` integer: Offers accepted and stored.
    - `unmatched` 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.
      - `sku` 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.
      - `targetKind` 'attachment' | 'firearm': What a seller listing is for.
      - `targetId` string: The id the row named, which we do not hold.

## Other responses

- `400`: Validation error
- `401`: Missing or invalid API key
- `403`: The credential is valid and the caller is not permitted. Three causes, none of which reissuing a key changes: the plan is below the endpoint's tier, the account is suspended, or the key is not attached to the thing being acted on, meaning a user account for `/v1/me` or a shop for `/v1/vendor`. Distinct from 401, which means the credential is missing, malformed or expired and a new one can work.
- `413`: The request body is larger than this endpoint accepts, which is 1 MB for a JSON body. Refused from `Content-Length` before the body is read, so nothing was stored and the request can be split and resent. `error.maxBytes` carries the limit.
- `429`: A limit was reached. `RATE_LIMITED` is the per-minute limit: wait `Retry-After` seconds (a minute) and retry. `DAILY_CAP_EXCEEDED` is the plan's daily limit, per key: nothing will be served until the counters return to zero at midnight UTC. `MONTHLY_CAP_EXCEEDED` is the plan's monthly allowance, for the whole account: nothing will be served until midnight UTC on the first of next month, or until the plan changes. For both, `Retry-After` is the seconds until then and `error.resetsAt` says when as an instant. Calls made after a refusal are refused too and do not spend the month, and a free key that keeps calling is paused, so stop at the first one and resume at the reset.
- `500`: Internal server error

---

[API](https://skmtc.dev/gunspec/apis/gunspec-io-api.md) · [All operations](https://skmtc.dev/gunspec/apis/gunspec-io-api/llms.txt) · [OpenAPI document](https://skmtc.dev/gunspec/apis/gunspec-io-api/revisions/f2e974d8b05c?raw)
