Intercept storefront 404s with priority-ordered glob redirects
/admin/api/redirect-rules
List every redirect rule, ordered by priority ascending. Includes hit count and last-match timestamp for each rule.
/admin/api/redirect-rules/{id}
Fetch a single redirect rule by id.
/admin/api/redirect-rules
Create a redirect rule. Patterns may use glob segments (e.g. /products/**) and the destination may reference capture groups via ${1}, ${2}, …
/admin/api/redirect-rules/{id}
Partial update of a redirect rule. Any field may be omitted; omitted fields are left unchanged. The combined (pattern, destination) shape is re-validated after the overlay.
/admin/api/redirect-rules/{id}
Permanently delete a redirect rule.
/admin/api/redirect-rules/test
Dry-run the current rule set against a candidate path without recording a hit. Returns the first matching rule's substituted destination, status code, and id, or `matched: false` if nothing matches.
/admin/api/redirect-rules/scan-loops
Scan the enabled rule set for redirect chains that loop back on themselves. Each rule is given a probe path and the matcher is walked forward until a previously seen rule fires, signalling a cycle. Read-only — records no hits and changes no rules. Returns an empty `cycles` array when the rule set is loop-free.
/admin/api/redirect-rules/auto-fix-loops
Break detected redirect loops by disabling the leading rule (lowest priority, ties broken by lowest id) of each supplied cycle. Two-step: send `confirm: false` (the default) for a dry-run preview of which rules would be disabled, then resend with `confirm: true` to execute. Pass the `cycles` returned by scan-loops; the server re-detects and aborts with 409 if the loop set has changed since you scanned.
/admin/api/redirect-rules
List every redirect rule, ordered by priority ascending. Includes hit count and last-match timestamp for each rule.
[
{
"id": 1,
"pattern": "/products/**",
"destination": "/p/${1}",
"status_code": 301,
"priority": 100,
"enabled": true,
"hit_count": 42,
"last_hit_at": "2026-05-02T18:30:00Z",
"created_at": "2026-04-15T09:00:00Z",
"updated_at": "2026-04-15T09:00:00Z"
},
{
"id": 2,
"pattern": "/old-blog",
"destination": "https://blog.example.com",
"status_code": 302,
"priority": 200,
"enabled": false,
"hit_count": 0,
"last_hit_at": null,
"created_at": "2026-04-20T11:15:00Z",
"updated_at": "2026-04-20T11:15:00Z"
}
]
curl "https://yourshop.zeroshop.io/admin/api/redirect-rules" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/redirect-rules",
headers={"Authorization": "Bearer zspat_..."}
)
rules = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/redirect-rules", {
headers: { "Authorization": "Bearer zspat_..." }
});
const rules = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/redirect-rules")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
rules = JSON.parse(resp.body)
/admin/api/redirect-rules/{id}
Fetch a single redirect rule by id.
{
"id": 1,
"pattern": "/products/**",
"destination": "/p/${1}",
"status_code": 301,
"priority": 100,
"enabled": true,
"hit_count": 42,
"last_hit_at": "2026-05-02T18:30:00Z",
"created_at": "2026-04-15T09:00:00Z",
"updated_at": "2026-04-15T09:00:00Z"
}
curl "https://yourshop.zeroshop.io/admin/api/redirect-rules/1" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/redirect-rules/1",
headers={"Authorization": "Bearer zspat_..."}
)
rule = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/redirect-rules/1", {
headers: { "Authorization": "Bearer zspat_..." }
});
const rule = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/redirect-rules/1")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
rule = JSON.parse(resp.body)
/admin/api/redirect-rules
Create a redirect rule. Patterns may use glob segments (e.g. /products/**) and the destination may reference capture groups via ${1}, ${2}, …
{
"pattern": "/products/**",
"destination": "/p/${1}",
"status_code": 301,
"priority": 100,
"enabled": true
}
{
"id": 7,
"pattern": "/products/**",
"destination": "/p/${1}",
"status_code": 301,
"priority": 100,
"enabled": true,
"hit_count": 0,
"last_hit_at": null,
"created_at": "2026-05-03T10:00:00Z",
"updated_at": "2026-05-03T10:00:00Z"
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/redirect-rules" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{
"pattern": "/products/**",
"destination": "/p/${1}",
"status_code": 301,
"priority": 100
}'
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/redirect-rules",
headers={"Authorization": "Bearer zspat_..."},
json={
"pattern": "/products/**",
"destination": "/p/${1}",
"status_code": 301,
"priority": 100
}
)
rule = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/redirect-rules", {
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
pattern: "/products/**",
destination: "/p/${1}",
status_code: 301,
priority: 100
})
});
const rule = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/redirect-rules")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = {
pattern: "/products/**",
destination: "/p/${1}",
status_code: 301,
priority: 100
}.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
rule = JSON.parse(resp.body)
/admin/api/redirect-rules/{id}
Partial update of a redirect rule. Any field may be omitted; omitted fields are left unchanged. The combined (pattern, destination) shape is re-validated after the overlay.
{
"enabled": false
}
{
"id": 1,
"pattern": "/products/**",
"destination": "/p/${1}",
"status_code": 301,
"priority": 100,
"enabled": false,
"hit_count": 42,
"last_hit_at": "2026-05-02T18:30:00Z",
"created_at": "2026-04-15T09:00:00Z",
"updated_at": "2026-05-03T10:05:00Z"
}
curl -X PUT "https://yourshop.zeroshop.io/admin/api/redirect-rules/1" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"enabled": false}'
import requests
resp = requests.put(
"https://yourshop.zeroshop.io/admin/api/redirect-rules/1",
headers={"Authorization": "Bearer zspat_..."},
json={"enabled": False}
)
rule = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/redirect-rules/1", {
method: "PUT",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ enabled: false })
});
const rule = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/redirect-rules/1")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { enabled: false }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
rule = JSON.parse(resp.body)
/admin/api/redirect-rules/{id}
Permanently delete a redirect rule.
curl -X DELETE "https://yourshop.zeroshop.io/admin/api/redirect-rules/1" \
-H "Authorization: Bearer zspat_..."
import requests
requests.delete(
"https://yourshop.zeroshop.io/admin/api/redirect-rules/1",
headers={"Authorization": "Bearer zspat_..."}
)
await fetch("https://yourshop.zeroshop.io/admin/api/redirect-rules/1", {
method: "DELETE",
headers: { "Authorization": "Bearer zspat_..." }
});
require "net/http"
uri = URI("https://yourshop.zeroshop.io/admin/api/redirect-rules/1")
req = Net::HTTP::Delete.new(uri)
req["Authorization"] = "Bearer zspat_..."
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/redirect-rules/test
Dry-run the current rule set against a candidate path without recording a hit. Returns the first matching rule's substituted destination, status code, and id, or `matched: false` if nothing matches.
{
"path": "/products/red-shoes",
"query": "color=red"
}
{
"matched": true,
"destination": "/p/red-shoes?color=red",
"status_code": 301,
"rule_id": 1
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/redirect-rules/test" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"path": "/products/red-shoes", "query": "color=red"}'
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/redirect-rules/test",
headers={"Authorization": "Bearer zspat_..."},
json={"path": "/products/red-shoes", "query": "color=red"}
)
result = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/redirect-rules/test", {
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ path: "/products/red-shoes", query: "color=red" })
});
const result = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/redirect-rules/test")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { path: "/products/red-shoes", query: "color=red" }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
result = JSON.parse(resp.body)
/admin/api/redirect-rules/scan-loops
Scan the enabled rule set for redirect chains that loop back on themselves. Each rule is given a probe path and the matcher is walked forward until a previously seen rule fires, signalling a cycle. Read-only — records no hits and changes no rules. Returns an empty `cycles` array when the rule set is loop-free.
{
"cycles": [
{
"rule_ids": [
5,
12
],
"hops": [
{
"rule_id": 5,
"matched_path": "/a",
"resolved_destination": "/b"
},
{
"rule_id": 12,
"matched_path": "/b",
"resolved_destination": "/a"
}
]
}
]
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/redirect-rules/scan-loops" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/redirect-rules/scan-loops",
headers={"Authorization": "Bearer zspat_..."}
)
result = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/redirect-rules/scan-loops", {
method: "POST",
headers: { "Authorization": "Bearer zspat_..." }
});
const result = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/redirect-rules/scan-loops")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
result = JSON.parse(resp.body)
/admin/api/redirect-rules/auto-fix-loops
Break detected redirect loops by disabling the leading rule (lowest priority, ties broken by lowest id) of each supplied cycle. Two-step: send `confirm: false` (the default) for a dry-run preview of which rules would be disabled, then resend with `confirm: true` to execute. Pass the `cycles` returned by scan-loops; the server re-detects and aborts with 409 if the loop set has changed since you scanned.
{
"confirm": true,
"cycles": [
{
"rule_ids": [
5,
12
],
"hops": [
{
"rule_id": 5,
"matched_path": "/a",
"resolved_destination": "/b"
},
{
"rule_id": 12,
"matched_path": "/b",
"resolved_destination": "/a"
}
]
}
]
}
{
"confirmed": true,
"disabled_rules": [
{
"id": 5,
"pattern": "/a",
"destination": "/b",
"priority": 1
}
]
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/redirect-rules/auto-fix-loops" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{
"confirm": true,
"cycles": [
{
"rule_ids": [5, 12],
"hops": [
{"rule_id": 5, "matched_path": "/a", "resolved_destination": "/b"},
{"rule_id": 12, "matched_path": "/b", "resolved_destination": "/a"}
]
}
]
}'
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/redirect-rules/auto-fix-loops",
headers={"Authorization": "Bearer zspat_..."},
json={
"confirm": True,
"cycles": [
{
"rule_ids": [5, 12],
"hops": [
{"rule_id": 5, "matched_path": "/a", "resolved_destination": "/b"},
{"rule_id": 12, "matched_path": "/b", "resolved_destination": "/a"}
]
}
]
}
)
result = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/redirect-rules/auto-fix-loops", {
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
confirm: true,
cycles: [
{
rule_ids: [5, 12],
hops: [
{ rule_id: 5, matched_path: "/a", resolved_destination: "/b" },
{ rule_id: 12, matched_path: "/b", resolved_destination: "/a" }
]
}
]
})
});
const result = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/redirect-rules/auto-fix-loops")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = {
confirm: true,
cycles: [
{
rule_ids: [5, 12],
hops: [
{ rule_id: 5, matched_path: "/a", resolved_destination: "/b" },
{ rule_id: 12, matched_path: "/b", resolved_destination: "/a" }
]
}
]
}.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
result = JSON.parse(resp.body)
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.