# Vendor

Vendor (seller) resource for the GunSpec SDK. Wraps `/v1/vendor/*` and the outbound click route. A seller holds an Enterprise plan and names one of their ordinary keys in Profile > Seller; that mapping is the whole vendor scope, so there is no separate credential to configure here. A key no shop names answers 403 `KEY_NOT_LINKED_TO_SHOP`. A feed reads before it writes: `shops()` tells a script which ids it can act for, `listOffers()` returns what the API holds, and `updateOffer()` changes one field without resending the rest.

Source: https://docs.gunspec.io/en/sdk/vendor

## client.vendor.shops()

`GET /v1/vendor/shops`

Auth: API key required  
Tier: Enterprise

The shops this key may act for. A script learns its shop ids here.

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object[] | The response payload. |
| `data[].id` | string | Slug of the shop. This is the value to send as `?vendor=`. |
| `data[].name` | string | Display name, as the maker writes it. |
| `data[].website` | string \| null | Official website, where one is known. |
| `data[].currency` | 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. |
| `data[].approved` | boolean | Whether staff have approved the shop. An unapproved shop accepts writes but shows nothing publicly. |

```json
{
  "success": true,
  "data": [
    {
      "id": "ridgeline-optics",
      "name": "Ridgeline Optics",
      "website": "https://ridgeline.example",
      "currency": "AUD",
      "approved": true
    }
  ]
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/vendor/shops' \
  --header 'X-API-Key: your_key'
```

## client.vendor.listOffers()

`GET /v1/vendor/offers`

Auth: API key required  
Tier: Enterprise

Your listings as the API holds them, `stockQty` and click counts included.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `vendor` | string | no | 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 |
| `kind` | string | no | Only parts or only firearms |
| `status` | string | no | Only drafts or only published rows |
| `q` | string | no | Match on your SKU or the product name |
| `page` | integer | no | Page number, from 1 to 10,000 |
| `per_page` | integer | no | Items per page (max 100) |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object[] | One page of results |
| `data[].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. |
| `data[].targetKind` | OfferTargetKind | What a seller listing is for. |
| `data[].targetId` | string | Slug of the catalogued record the listing points at, in whichever catalog `targetKind` names. |
| `data[].priceCents` | 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. |
| `data[].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. |
| `data[].url` | string | Absolute, directly fetchable URL. |
| `data[].inStock` | boolean | Whether the shop reports it in stock. Derived from `stockQty` where the shop tracks a count. |
| `data[].stockQty` | 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. |
| `data[].status` | OfferStatus | Whether a seller listing is public. |
| `data[].source` | string | Where the record came from: a curated entry, an inference, or an import. |
| `data[].clicks` | integer | Outbound clicks this listing earned over the last 30 days. |
| `data[].updatedAt` | string | When a field we serve last changed. |
| `pagination.page` | number | Current page number |
| `pagination.per_page` | number | Items per page |
| `pagination.total` | number | Total matching records (Builder and above) |

```json
{
  "success": true,
  "data": [
    {
      "sku": "AIM-ACRO-P2",
      "targetKind": "attachment",
      "targetId": "aimpoint-acro-p-2",
      "priceCents": 129900,
      "currency": "AUD",
      "url": "https://example-optics.com/products/aimpoint-acro-p-2",
      "inStock": true,
      "stockQty": 4,
      "status": "draft",
      "source": "api",
      "clicks": 37,
      "updatedAt": "2026-09-08 04:11:00"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "per_page": 20,
    "total": 42,
    "totalPages": 3
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/vendor/offers?vendor=ridgeline-optics' \
  --header 'X-API-Key: your_key'
```

## client.vendor.pushOffers()

`PUT /v1/vendor/offers`

Auth: API key required  
Tier: Enterprise

Upsert up to 500 listings keyed by your own SKU. Ids we do not hold come back as `unmatched` rather than failing the batch; a bad row (a string price, a float) is a 400 with `details` naming it.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `vendor` | string | no | 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 |

### Request body (application/json)

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `offers` | object[] | yes | The listings to upsert, keyed by your own SKU. Up to 500 per request. |

```json
{
  "offers": [
    {
      "sku": "AIM-ACRO-P2",
      "attachment_id": "aimpoint-acro-p-2",
      "price_cents": 129900,
      "currency": "AUD",
      "url": "https://example-optics.com/products/aimpoint-acro-p-2",
      "in_stock": true,
      "region": "AU"
    }
  ]
}
```

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.written` | integer | Offers accepted and stored. |
| `data.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. |
| `data.unmatched[].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. |
| `data.unmatched[].targetKind` | OfferTargetKind | What a seller listing is for. |
| `data.unmatched[].targetId` | string | The id the row named, which we do not hold. |

```json
{
  "success": true,
  "data": {
    "written": 498,
    "unmatched": [
      {
        "sku": "OLD-SKU-1",
        "targetKind": "attachment",
        "targetId": "not-in-catalog"
      }
    ]
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 413 | `PAYLOAD_TOO_LARGE` | Request body too large |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request PUT \
  --url 'https://api.gunspec.io/v1/vendor/offers?vendor=ridgeline-optics' \
  --header 'X-API-Key: your_key' \
  --header 'Content-Type: application/json' \
  --data '
{
  "offers": [
    {
      "sku": "AIM-ACRO-P2",
      "attachment_id": "aimpoint-acro-p-2",
      "price_cents": 129900,
      "currency": "AUD",
      "url": "https://example-optics.com/products/aimpoint-acro-p-2",
      "in_stock": true,
      "region": "AU"
    }
  ]
}
'
```

## client.vendor.updateOffer()

`PATCH /v1/vendor/offers/{sku}`

Auth: API key required  
Tier: Enterprise

Change one listing. Absent fields are unchanged.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `sku` | string | yes | Your own SKU for the offer |

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `vendor` | string | no | 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 |

### Request body (application/json)

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `price_cents` | integer | no | New price in integer minor units of `currency`. A float is refused. |
| `currency` | string | no | 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 | no | Product page on your shop. Must be a full https:// address; any other scheme is refused. |
| `stock_qty` | integer | no | 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. |
| `in_stock` | boolean | no | Stock flag, for a shop that does not count units. Send this or `stock_qty`, never both. |
| `region` | string \| null | no | ISO 3166-1 alpha-2 code you ship to. Null states no restriction. |
| `status` | string | no | Set `published` to show the listing on public pages, or `draft` to take it off them while keeping it. |

```json
{
  "price_cents": 139900,
  "currency": "AUD",
  "url": "https://example-optics.com/products/aimpoint-acro-p-2",
  "stock_qty": 0,
  "in_stock": false,
  "region": "AU",
  "status": "published"
}
```

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.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. |

```json
{
  "success": true,
  "data": {
    "sku": "AIM-ACRO-P2"
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 413 | `PAYLOAD_TOO_LARGE` | Request body too large |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request PATCH \
  --url 'https://api.gunspec.io/v1/vendor/offers/AIM-ACRO-P2?vendor=ridgeline-optics' \
  --header 'X-API-Key: your_key' \
  --header 'Content-Type: application/json' \
  --data '
{
  "price_cents": 139900,
  "currency": "AUD",
  "url": "https://example-optics.com/products/aimpoint-acro-p-2",
  "stock_qty": 0,
  "in_stock": false,
  "region": "AU",
  "status": "published"
}
'
```

## client.vendor.deleteOffer()

`DELETE /v1/vendor/offers/{sku}`

Auth: API key required  
Tier: Enterprise

Withdraw one listing.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `sku` | string | yes | Your own SKU for the offer |

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `vendor` | string | no | 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 |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.removed` | boolean | Always true: the SKU is not listed once this returns, whether or not it was before. |

```json
{
  "success": true,
  "data": {
    "removed": true
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request DELETE \
  --url 'https://api.gunspec.io/v1/vendor/offers/AIM-ACRO-P2?vendor=ridgeline-optics' \
  --header 'X-API-Key: your_key'
```

## client.vendor.clickUrl()

`GET /v1/out/{clickId}`

Auth: Public  
Tier: Explorer+

The tracked outbound link for an offer's `clickId`. Render this as the href: the API counts the visit and 302s to the shop, so the seller can check our figure against their own analytics.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `clickId` | string | yes | The listing's tracking token, from `clickId` on an offer |

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `l` | string | no | Locale for the catalog page a lapsed listing falls back to |

### Response 302 (application/json)

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/out/example'
```

## client.vendor.resolveClick()

`GET /v1/out/{clickId}`

Auth: Public  
Tier: Explorer+

Follow a click server-side and return the shop URL it lands on. The API counts this as a visit exactly as a browser's would; use it for a link checker, not for rendering. `null` when the runtime hides cross-origin redirects (browsers do).

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `clickId` | string | yes | The listing's tracking token, from `clickId` on an offer |

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `l` | string | no | Locale for the catalog page a lapsed listing falls back to |

### Response 302 (application/json)

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/out/example'
```
