404 Log

Capture storefront 404s, triage them, and map them to redirect rules

GET /admin/api/404-log

List captured 404 entries for the given tab, paginated. Each entry has a path, rolling hit count, first/last-seen timestamps, last referer + user agent, and (if mapped) the redirect rule id and timestamp it was mapped at.

DELETE /admin/api/404-log/{id}

Permanently delete a single 404 log entry. Subsequent hits to the same path will create a fresh entry.

POST /admin/api/404-log/{id}/map

Create a redirect rule from this 404 entry and link the entry to the new rule. The rule's pattern defaults to the entry's captured path (override via `pattern`); the destination is resolved from `target_type` + (`target_id` or `destination`). Soft links to products / categories / pages survive slug changes — the storefront re-resolves them at request time. Responds with the new rule, identical in shape to POST /admin/api/redirect-rules.

POST /admin/api/404-log/{id}/suppress

Suppress a log entry. The entry stops appearing on the unmapped tab (move to `tab=suppressed` to find it) but the shop keeps incrementing its hit count when fresh 404s arrive. Use this for noisy bot probes you don't want cluttering triage.

POST /admin/api/404-log/{id}/unsuppress

Reverse a previous suppress. The entry returns to the unmapped tab so it can be triaged again.

POST /admin/api/404-log/import/preview

Upload a CSV of known-broken URLs as a multipart form (single `file` field) and receive a preview summary — number of rows, how many are new vs. already-tracked, validation errors per row — with no database writes. Maximum upload size is 20 MB. Use this to let the merchant eyeball the import before committing.

POST /admin/api/404-log/import/confirm

Apply a previously-previewed import. The pipeline re-runs against the freshly uploaded `file` (drift-safe — the merchant uploads the same file again) and inserts new entries in a single transaction. If, after re-running, there is nothing left to import (e.g. another admin imported the same file in the meantime), the server returns 422 with the fresh preview body so the UI can fall back to the preview phase.

GET /admin/api/404-log/export.csv

Download every row in a tab as a CSV file (no paging). Unlike the JSON list endpoint, the response Content-Type is `text/csv; charset=utf-8` and the body is sent as an attachment (`Content-Disposition: attachment; filename="404-log-<tab>.csv"`). Columns are: `path, hit_count, first_seen_at, last_seen_at, last_referer, last_user_agent, suppressed, mapped_rule_id, mapped_destination`. The `mapped_destination` column holds the literal destination of the linked redirect rule and is populated only for mapped entries; empty optional fields render as empty cells. The resulting file can be fed back into the import endpoint.

GET /admin/api/404-log

List captured 404 entries for the given tab, paginated. Each entry has a path, rolling hit count, first/last-seen timestamps, last referer + user agent, and (if mapped) the redirect rule id and timestamp it was mapped at.

Parameters

Name Type Required Description
tab string optional Which bucket to list: `unmapped` (default), `mapped`, or `suppressed`.
limit integer optional Page size, clamped to 1..200. Defaults to 50.
offset integer optional Zero-based offset for pagination. Defaults to 0.

Response 200

{
  "entries": [
    {
      "id": 12,
      "path": "/old-product-page",
      "hit_count": 37,
      "first_seen_at": "2026-04-21T09:14:00Z",
      "last_seen_at": "2026-05-12T22:08:00Z",
      "last_referer": "https://www.google.com/",
      "last_user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
      "mapped_rule_id": null,
      "mapped_at": null,
      "suppressed": false
    },
    {
      "id": 14,
      "path": "/promo/spring-2024",
      "hit_count": 4,
      "first_seen_at": "2026-05-01T11:02:00Z",
      "last_seen_at": "2026-05-10T16:44:00Z",
      "last_referer": null,
      "last_user_agent": "curl/8.4.0",
      "mapped_rule_id": null,
      "mapped_at": null,
      "suppressed": false
    }
  ],
  "total": 23
}

Example

curl "https://yourshop.zeroshop.io/admin/api/404-log?tab=unmapped&limit=50" \
  -H "Authorization: Bearer zspat_..."
DELETE /admin/api/404-log/{id}

Permanently delete a single 404 log entry. Subsequent hits to the same path will create a fresh entry.

Response 204

Example

curl -X DELETE "https://yourshop.zeroshop.io/admin/api/404-log/12" \
  -H "Authorization: Bearer zspat_..."
POST /admin/api/404-log/{id}/map

Create a redirect rule from this 404 entry and link the entry to the new rule. The rule's pattern defaults to the entry's captured path (override via `pattern`); the destination is resolved from `target_type` + (`target_id` or `destination`). Soft links to products / categories / pages survive slug changes — the storefront re-resolves them at request time. Responds with the new rule, identical in shape to POST /admin/api/redirect-rules.

Request Body

{
  "target_type": "product",
  "target_id": 42,
  "status_code": 301
}

Response 201

{
  "id": 7,
  "pattern": "/old-product-page",
  "destination": "/p/new-product-slug",
  "status_code": 301,
  "priority": 100,
  "enabled": true,
  "hit_count": 0,
  "last_hit_at": null,
  "created_at": "2026-05-13T10:00:00Z",
  "updated_at": "2026-05-13T10:00:00Z"
}

Example

curl -X POST "https://yourshop.zeroshop.io/admin/api/404-log/12/map" \
  -H "Authorization: Bearer zspat_..." \
  -H "Content-Type: application/json" \
  -d '{"target_type": "product", "target_id": 42, "status_code": 301}'
POST /admin/api/404-log/{id}/suppress

Suppress a log entry. The entry stops appearing on the unmapped tab (move to `tab=suppressed` to find it) but the shop keeps incrementing its hit count when fresh 404s arrive. Use this for noisy bot probes you don't want cluttering triage.

Response 204

Example

curl -X POST "https://yourshop.zeroshop.io/admin/api/404-log/12/suppress" \
  -H "Authorization: Bearer zspat_..."
POST /admin/api/404-log/{id}/unsuppress

Reverse a previous suppress. The entry returns to the unmapped tab so it can be triaged again.

Response 204

Example

curl -X POST "https://yourshop.zeroshop.io/admin/api/404-log/12/unsuppress" \
  -H "Authorization: Bearer zspat_..."
POST /admin/api/404-log/import/preview

Upload a CSV of known-broken URLs as a multipart form (single `file` field) and receive a preview summary — number of rows, how many are new vs. already-tracked, validation errors per row — with no database writes. Maximum upload size is 20 MB. Use this to let the merchant eyeball the import before committing.

Response 200

{
  "total_rows": 120,
  "new_entries": 98,
  "existing_entries": 18,
  "invalid_rows": [
    {
      "row": 7,
      "error": "path must start with /"
    },
    {
      "row": 41,
      "error": "duplicate path within file"
    }
  ],
  "sample": [
    {
      "path": "/legacy/widgets/red"
    },
    {
      "path": "/legacy/widgets/blue"
    },
    {
      "path": "/blog/2019/launch"
    }
  ]
}

Example

curl -X POST "https://yourshop.zeroshop.io/admin/api/404-log/import/preview" \
  -H "Authorization: Bearer zspat_..." \
  -F "file=@broken-urls.csv"
POST /admin/api/404-log/import/confirm

Apply a previously-previewed import. The pipeline re-runs against the freshly uploaded `file` (drift-safe — the merchant uploads the same file again) and inserts new entries in a single transaction. If, after re-running, there is nothing left to import (e.g. another admin imported the same file in the meantime), the server returns 422 with the fresh preview body so the UI can fall back to the preview phase.

Response 200

{
  "inserted": 98,
  "skipped_existing": 18,
  "skipped_invalid": 4
}

Example

curl -X POST "https://yourshop.zeroshop.io/admin/api/404-log/import/confirm" \
  -H "Authorization: Bearer zspat_..." \
  -F "file=@broken-urls.csv"
GET /admin/api/404-log/export.csv

Download every row in a tab as a CSV file (no paging). Unlike the JSON list endpoint, the response Content-Type is `text/csv; charset=utf-8` and the body is sent as an attachment (`Content-Disposition: attachment; filename="404-log-<tab>.csv"`). Columns are: `path, hit_count, first_seen_at, last_seen_at, last_referer, last_user_agent, suppressed, mapped_rule_id, mapped_destination`. The `mapped_destination` column holds the literal destination of the linked redirect rule and is populated only for mapped entries; empty optional fields render as empty cells. The resulting file can be fed back into the import endpoint.

Parameters

Name Type Required Description
tab string optional Which bucket to export: `unmapped` (default), `mapped`, or `suppressed`. Returns every row in the tab without pagination.

Response 200

"path,hit_count,first_seen_at,last_seen_at,last_referer,last_user_agent,suppressed,mapped_rule_id,mapped_destination\n/old-product-page,37,2026-04-21T09:14:00Z,2026-05-12T22:08:00Z,https://www.google.com/,Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36,false,7,/p/new-product-slug\n/promo/spring-2024,4,2026-05-01T11:02:00Z,2026-05-10T16:44:00Z,,curl/8.4.0,false,,\n"

Example

curl "https://yourshop.zeroshop.io/admin/api/404-log/export.csv?tab=unmapped" \
  -H "Authorization: Bearer zspat_..." \
  -o 404-log-unmapped.csv

We value your privacy

We use cookies for essential site functionality and, with your consent, analytics to understand how our platform is used. No personal data is shared with third parties. See our Privacy Policy for details.