# Redirects and sitemap

These two endpoints help a storefront keep its search ranking when the merchant renames things. Borkol speaks in storefront paths of a fixed shape: a product lives at `/products/{slug}` and a category at `/categories/{slug}`. If your storefront uses different URLs, map these paths onto your own routes.

When the merchant changes a product or category slug, Borkol records a permanent redirect from the old path to the new one. 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 use the catalog authentication: a tenant API key carrying either the `catalog:read` or the `commerce:read` scope, sent as `Authorization: Bearer {api_key}`, and the catalog module enabled for the store. 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 or category 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`. 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 product's or category's status, and archiving 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 neither the `catalog:read` nor the `commerce:read` scope. |
| 403 | `{"error":"Module not enabled: catalog"}` | Tenant does not have the catalog module enabled. |
| 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 catalog path, ready to render as a `sitemap.xml`: all products with status `active` and all categories that are active. Products come first, then categories, each group ordered by slug. 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"
    }
  ]
}
```

### Response fields

Each entry in `data`:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| type | string | always | `product` or `category`. |
| path | string | always | Storefront path: `/products/{slug}` or `/categories/{slug}`. |
| lastmod | string (ISO 8601) or null | always | When the product or category was last updated. |

### Errors

| Status | Body | Cause |
| --- | --- | --- |
| 401 | `{"message":"Unauthenticated."}` | Missing or invalid API key. |
| 403 | `{"message":"Invalid ability provided."}` | Key carries neither the `catalog:read` nor the `commerce:read` scope. |
| 403 | `{"error":"Module not enabled: catalog"}` | Tenant does not have the catalog module enabled. |
| 403 | `{"message":"Tenant suspended.","code":"tenant_suspended"}` | Tenant account is suspended. |
| 429 | `{"message":"Too Many Attempts."}` | Rate limit of 120 requests per minute exceeded. |
