# Redirects and sitemap

These two endpoints help a storefront keep its search ranking when the merchant renames things. They belong to the platform, not to one module: any module that gives content an address contributes to them. Catalog content uses paths of a fixed shape: a product lives at `/products/{slug}` and a category at `/categories/{slug}`. Content records (Objects) live at the type's address prefix for the language plus the record slug, for example `/services/kitchen-fitting`. If your storefront uses different URLs, map these paths onto your own routes.

The catalog contributes sitemap entries for products and categories, and Objects contributes one entry per published record address (see [GET /v1/api/sitemap](#get-v1apisitemap)). When the merchant changes a product, category or record slug, Borkol records a permanent redirect from the old path to the new one; for a record this happens only when the type has a non-empty address prefix for that language. Chains collapse: renaming `a` to `b` and then `b` to `c` leaves both `/products/a` and `/products/b` pointing straight at `/products/c`. A path that becomes live again (a new product takes the old slug) stops redirecting.

Both endpoints take a tenant API key carrying any one of the `catalog:read`, `commerce:read`, `tenant:read` or `objects:read` scopes, sent as `Authorization: Bearer {api_key}`. They do not require the catalog module: a store that only uses Objects can call them too. The shared tenant rate limit of 120 requests per minute applies.

## GET /v1/api/redirects

Looks up the redirect for one storefront path. Call it when a product, category or record request on your storefront would otherwise answer 404, and send the visitor on with the returned `status` code.

The lookup strips one trailing slash (`/products/old-chair/` matches `/products/old-chair`; the root path `/` is left alone) and is case-sensitive. Only exact paths match: query strings are not part of the path.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| path | string | Yes | The storefront path to look up, for example `/products/old-chair` or `/services/old-slug`. At most 512 characters. |

```bash title="Request"
curl "https://api.borkol.com/v1/api/redirects?path=%2Fproducts%2Fold-chair" \
  -H "Authorization: Bearer {api_key}" \
  -H "Accept: application/json"
```

```javascript title="Request (JS)"
const res = await fetch(
  "https://api.borkol.com/v1/api/redirects?" +
    new URLSearchParams({ path: "/products/old-chair" }),
  {
    headers: {
      Authorization: `Bearer ${apiKey}`,
      Accept: "application/json",
    },
  }
);
if (res.status === 404) {
  // No redirect: render your own not-found page.
} else {
  const { data } = await res.json();
  // Redirect the visitor to data.to_path with data.status.
}
```

```json title="Response"
{
  "data": {
    "from_path": "/products/old-chair",
    "to_path": "/products/oak-dining-chair",
    "status": 301
  }
}
```

### Response fields

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| from_path | string | always | The stored old path (without a trailing slash). |
| to_path | string | always | The path to send the visitor to. Redirects are recorded whatever the status of the product, category or record, and archiving or unpublishing does not remove one, so `to_path` itself may be unavailable; check it before redirecting. |
| status | integer | always | HTTP status to redirect with. Currently always `301` (permanent). |

### Errors

| Status | Body | Cause |
| --- | --- | --- |
| 401 | `{"message":"Unauthenticated."}` | Missing or invalid API key. |
| 403 | `{"message":"Invalid ability provided."}` | Key carries none of the `catalog:read`, `commerce:read`, `tenant:read` or `objects:read` scopes. |
| 403 | `{"message":"Tenant suspended.","code":"tenant_suspended"}` | Tenant account is suspended. |
| 404 | `{"message":"..."}` | No redirect exists for this path. |
| 422 | `{"message":"...","errors":{"path":["..."]}}` | `path` is missing, not a string, or longer than 512 characters. |
| 429 | `{"message":"Too Many Attempts."}` | Rate limit of 120 requests per minute exceeded. |

## GET /v1/api/sitemap

Lists every live path of the modules enabled for the store, ready to render as a `sitemap.xml`. A module that is disabled contributes nothing, so a store without the catalog module gets no product or category entries (the response is then an empty `data` array, not an error). The catalog module contributes all products with status `active` and all categories that are active, products first, then categories, each group ordered by slug. The Objects module contributes one entry per publicly visible record, language and address, for object types that have a non-empty address prefix for that language; types without prefixes have no public pages and are left out, and so are drafts and scheduled records. The response is not paginated.

### Parameters

None.

```bash title="Request"
curl "https://api.borkol.com/v1/api/sitemap" \
  -H "Authorization: Bearer {api_key}" \
  -H "Accept: application/json"
```

```json title="Response"
{
  "data": [
    {
      "type": "product",
      "path": "/products/oak-dining-chair",
      "lastmod": "2026-10-11T09:14:33+00:00"
    },
    {
      "type": "category",
      "path": "/categories/chairs",
      "lastmod": "2026-10-02T15:20:11+00:00"
    },
    {
      "type": "object_record",
      "object_type": "service",
      "locale": "nl",
      "path": "/diensten/keukenmontage",
      "lastmod": "2026-10-09T12:41:07+00:00",
      "alternates": {
        "en": "/services/kitchen-fitting",
        "nl": "/diensten/keukenmontage"
      }
    }
  ]
}
```

### Response fields

Each entry in `data`:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| type | string | always | `product`, `category` or `object_record`. |
| path | string | always | Storefront path: `/products/{slug}`, `/categories/{slug}`, or for a record the type's address prefix for the language plus the record's address, for example `/services/kitchen-fitting`. |
| lastmod | string (ISO 8601) or null | always | When the product, category or record was last updated. |
| object_type | string | `object_record` only | Key of the record's object type. |
| locale | string | `object_record` only | The language of this entry's address. |
| alternates | object | `object_record` only | Language tag to path, for every enabled language in which the record has a public address (including this entry's own). Use it for `hreflang` links. |

### Errors

| Status | Body | Cause |
| --- | --- | --- |
| 401 | `{"message":"Unauthenticated."}` | Missing or invalid API key. |
| 403 | `{"message":"Invalid ability provided."}` | Key carries none of the `catalog:read`, `commerce:read`, `tenant:read` or `objects:read` scopes. |
| 403 | `{"message":"Tenant suspended.","code":"tenant_suspended"}` | Tenant account is suspended. |
| 429 | `{"message":"Too Many Attempts."}` | Rate limit of 120 requests per minute exceeded. |
