Resolve a slug collision with an incrementing numeric suffix
The slug collision resolver takes the exact slug you want and compares it with the slugs that are already occupied.
Run — free
When the desired value is available, it returns that value unchanged. When it is taken, the resolver checks numeric alternatives such as a trailing -2, -3, and onward until it finds the first available choice. It is a small, deterministic building block for publishing systems, import jobs, documentation generators, and any workflow that must assign a unique URL path without silently replacing existing content.
Preserve the desired slug whenever possible
A good collision resolver should change a URL only when it has to. Send the proposed value in desired_slug and provide every occupied value in used_slugs. The resolver performs exact, case-sensitive comparisons. If the proposed value does not appear in the occupied set, the result contains the original slug and reports that no collision occurred. It does not lowercase, trim, transliterate, or replace punctuation, because those transformations belong in a slugification step before uniqueness is checked. Keeping these responsibilities separate prevents unexpected URL changes and makes the result easy to reproduce in builds, migrations, and tests. An empty desired slug is rejected rather than turned into an arbitrary suffix, since such a result would hide a missing title or a broken upstream mapping. Duplicate entries in the used list do not affect the answer. They are collapsed naturally during lookup, so callers do not need to clean that list merely to obtain a stable result.
Choose the first available numeric suffix
When the exact desired slug is occupied, suffix selection begins at 2, following the familiar convention in which the unsuffixed path represents the first item and the next item receives -2. The resolver then checks candidates in ascending order: desired-2, desired-3, desired-4, and so forth. It stops at the first candidate absent from the supplied set. This means gaps are reused predictably. For example, if report, report-2, and report-4 are occupied, the returned slug is report-3. The comparison uses complete strings, so unrelated values such as annual-report or report-old never create a collision. Existing numeric endings are treated as part of the desired value rather than parsed or rewritten; if release-2 is requested and occupied, the first candidate is release-2-2. This literal behavior avoids guessing what a caller meant and guarantees that identical inputs always produce identical outputs, with no dependence on a database, clock, random generator, locale, or execution order.
Use the result safely in publishing workflows
This capability is useful at the point where your application already knows which slugs are reserved. Fetch or collect those values, pass them with the desired slug, and use the returned slug for the new record. The response also states whether a collision occurred and, for a suffixed result, which numeric suffix was selected. That metadata can support logs, previews, or a message explaining why a URL differs from the original proposal. The operation itself does not reserve the returned value, so systems handling concurrent writers should still enforce a unique constraint and retry with a refreshed used list if another writer claims the same slug. For batch imports, update your local occupied set after every successful assignment before resolving the next row. Exact comparison also means callers should apply their own URL policy consistently beforehand: normalize case, Unicode, spaces, or punctuation with a slugifier if the application requires it. Each API request costs $0.002, while the same deterministic logic can run in the browser for interactive checks.
What you can do with it
Publish a page without replacing another
Keep the editor's preferred path when it is free, or assign the first available numbered alternative when it is already occupied.
Import records with stable unique URLs
Resolve each prepared slug against existing and newly assigned paths so repeated titles receive deterministic URL suffixes.
Generate documentation routes
Prevent duplicate headings or generated pages from claiming the same route while preserving readable, predictable paths.
FAQ
What happens when the desired slug is free?
It is returned unchanged, and collision is false.
Which suffix is tried first?
The resolver starts with -2, then checks -3, -4, and higher values until it finds the first free candidate.
Does it convert text into a URL slug?
No. It compares and extends the exact value supplied. Run a slugifier first if you need case, whitespace, punctuation, or Unicode normalization.
Are comparisons case-sensitive?
Yes. Product and Product are identical, while Product and product are treated as different strings.
Does the capability reserve the returned slug?
No. It computes a candidate from the supplied list. Your storage layer should enforce uniqueness when the record is written.
What does an API request cost?
Each request costs $0.002.
For developers — API access
Everything on this page is available programmatically. This section is for teams who want to wire it into their own systems; everyone else can just use the tool above.
API endpoint
Prefer to automate it? One authenticated POST creates the task; the result comes back by webhook or a signed link. The same capability also runs here on the web, by email and from Telegram — and soon from our app too.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/dev/slug-collision-resolve \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"desired_slug":"product-guide","used_slugs":["product-guide","product-guide-2"]}'const res = await fetch("https://api.kit.forhosting.com/dev/slug-collision-resolve", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"desired_slug": "product-guide",
"used_slugs": [
"product-guide",
"product-guide-2"
]
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/dev/slug-collision-resolve",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"desired_slug": "product-guide",
"used_slugs": [
"product-guide",
"product-guide-2"
]
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/dev/slug-collision-resolve", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"desired_slug":"product-guide","used_slugs":["product-guide","product-guide-2"]}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"desired_slug":"product-guide","used_slugs":["product-guide","product-guide-2"]}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/dev/slug-collision-resolve", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"desired_slug": "product-guide",
"used_slugs": [
"product-guide",
"product-guide-2"
]
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "dev.slug_collision_resolve",
"status": "queued",
"_links": {
"result": "/tasks/tsk_…/result"
}
}The API is asynchronous: the call returns a task_id immediately and the result arrives by webhook. Polling is capped at 1 req/s per task.
Pricing
Published price — no tokens, no invented credits. A failed task is never charged.
Limits
max_used_slugs | 10000 |
max_slug_chars | 10000 |
Errors
| HTTP | Code | Meaning |
|---|---|---|
401 | unauthorized | Missing or invalid API key. |
402 | insufficient_balance | Your balance doesn't cover the task price. |
404 | unknown_type | That task type doesn't exist. |
429 | rate_limited | Too many requests. Use the webhook instead of polling. |