Shorten links, generate QR codes, and read click analytics over a simple REST API. Every account, including the free plan, gets an API key with no credit card. Upgrade to Pro for batch shortening, device routing, A/B split links, and app deep links with App Store fallback.
The Gajo REST API shortens URLs, manages links, and returns click analytics. Every Gajo account plan includes the core API, including the Free plan, no credit card required. Pro adds batch shortening, editing destinations, device routing, A/B split links, and app deep links.
Base URL
https://gajo.io/api/v1
Machine-readable spec: /openapi.json (OpenAPI 3.1)
Send your API key in the x-api-key header on every request. Create a free account, then
generate a key on the API Keys page.
curl -X POST "https://gajo.io/api/v1/shorten" \
--header "x-api-key: YOUR_API_KEY" \
--header "Content-Type: application/json" \
--data '{"url": "https://example.com"}'
| Free | Basic | Pro | |
|---|---|---|---|
| Price | $0, no card | $4.99 / month | $9.99 / month |
| URLs per month | 50 | 500 | 10,000 |
| Requests per minute | 10 | 30 | 60 |
| Max link lifetime | 60 days | 120 days | Unlimited |
| Short code length | 6 characters | 5 characters | 5 characters |
| Stats endpoint | Totals + 7-day history | Full: referrers, devices, geo | Full: referrers, devices, geo |
| Batch, edit, routing, templates, deep links | — | — | Yes |
The monthly URL allowance is shared with links created in the dashboard. Endpoints marked
Pro return 403 on other plans.
See pricing for full plan details.
Each key may make the plan's number of requests per 60-second sliding window. Every response includes
X-RateLimit-Limit and X-RateLimit-Remaining. Over the limit, the API returns
429 Too Many Requests with a Retry-After header (seconds).
POST /shorten also returns 429 once the monthly URL allowance is used up.
All responses share one envelope. Errors set success to false and explain why in message:
{
"success": false,
"message": "A Pro plan is required for batch shortening"
}
| Status | Meaning |
|---|---|
400 | Invalid request (e.g. unsupported URL scheme, bad lifetimeDays, or a destination flagged as phishing or malware by Google Web Risk) |
401 | Missing or invalid x-api-key |
403 | Endpoint or field requires a Pro plan |
404 | Short code not found, or it belongs to another account |
409 | The requested custom code is taken, released, or already linked to a URL |
429 | Rate limit or monthly URL allowance exceeded |
Connect Claude, Cursor, VS Code, or any MCP client to https://gajo.io/mcp
(Streamable HTTP) and send your API key as the x-api-key header. The assistant can then shorten
links and look up stats for you, on your plan's limits.
| Tool | What it does |
|---|---|
shorten_url | Create a short link (Pro: customCode, deep links) |
list_urls, get_url | Find your links |
get_url_stats | Click analytics, bots excluded |
update_url | Pro: change a link's destination |
deactivate_url | Turn a link off |
get_usage | Plan and remaining monthly quota |
Claude Code:
claude mcp add --transport http gajo https://gajo.io/mcp --header "x-api-key: YOUR_API_KEY"
Cursor (~/.cursor/mcp.json):
{
"mcpServers": {
"gajo": {
"url": "https://gajo.io/mcp",
"headers": { "x-api-key": "YOUR_API_KEY" }
}
}
}
VS Code (.vscode/mcp.json):
{
"servers": {
"gajo": {
"type": "http",
"url": "https://gajo.io/mcp",
"headers": { "x-api-key": "YOUR_API_KEY" }
}
}
}
Clients that only support bearer tokens can send Authorization: Bearer YOUR_API_KEY instead.
Tool calls share your key's per-minute rate limit with the REST API.
Your account and plan capabilities. Useful for testing credentials.
{
"success": true,
"data": {
"plan": "Free",
"email": "you@example.com",
"urlCount": 12,
"apiAccess": true,
"customCodes": false,
"routingRules": false,
"memberSince": "2026-03-22T12:00:00Z"
}
}
This month's usage against your plan. maxLinkLifetimeDays is null when unlimited.
{
"success": true,
"data": {
"plan": "Free",
"urlsCreatedThisMonth": 8,
"urlsMonthlyLimit": 50,
"urlsRemaining": 42,
"requestsPerMinute": 10,
"maxLinkLifetimeDays": 60,
"totalUrls": 12,
"totalClicks": 318
}
}
Create a short URL. Code length and lifetime follow your plan.
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Destination URL (max 2048 chars): http, https, mailto, tel, ftp, sftp. Pro may also use app deep links such as myapp://path. |
customCode | string | No | Pro: use a custom code (3-50 letters, digits, or hyphens; stored lowercase). The link redirects at https://gajo.io/c/{code}. An unclaimed code is claimed for you, and one of your own codes is reused if it isn't linked to a URL; otherwise 409 (change a linked code's destination with PUT /urls/{code}). Can't be combined with lifetimeDays. |
lifetimeDays | integer | No | Days until the link expires. Defaults to, and is capped at, your plan maximum (60 Free, 120 Basic, unlimited Pro). |
fallbackUrl | string | No | Pro, deep links only: a web URL (e.g. the App Store listing) used when the app isn't installed. |
{
"success": true,
"message": "URL shortened successfully",
"data": {
"shortCode": "AbCdEf",
"originalUrl": "https://example.com/page",
"fallbackUrl": null,
"active": true,
"clickCount": 0,
"isCustomCode": false,
"createdDate": "2026-10-07T12:00:00Z",
"expiresDate": "2026-12-06T12:00:00Z"
}
}
The short link is https://gajo.io/{shortCode}, or https://gajo.io/c/{shortCode} when isCustomCode is true.
Create up to 100 short URLs in one request. Each item takes the same fields as /shorten and reports its own success or error.
{
"urls": [
{ "url": "https://example.com/page1", "lifetimeDays": 90 },
{ "url": "https://example.com/page2" }
]
}
List your short URLs, newest first, a page at a time. data is an array of links.
| Query parameter | Default | Description |
|---|---|---|
page | 1 | Page number, starting at 1 |
limit | 100 | Links per page, up to 1000 |
The X-Total-Count header has the total number of links. When there are more, a
Link header points at the next page: </api/v1/urls?page=2&limit=100>; rel="next".
Get one of your short URLs by code.
Click analytics for a URL. Bot traffic (crawlers, link previews, AI fetchers) is excluded. Free returns
totalClicks and 7 days of clicksByDay. Basic and Pro return 30 days plus referrer,
device, country, and region breakdowns.
{
"success": true,
"data": {
"totalClicks": 318,
"historyDays": 30,
"clicksByDay": [ { "date": "2026-10-06", "clicks": 42 } ],
"topReferrers": [ { "source": "https://news.ycombinator.com/", "count": 120 } ],
"deviceBreakdown": [ { "device": "Mobile", "percentage": 61.2 } ],
"topCountries": [ { "country": "United States", "count": 140 } ],
"topRegions": [ { "region": "California, US", "count": 51 } ]
}
}
Change a link's destination or interstitial setting. Takes effect immediately.
| Field | Type | Description |
|---|---|---|
originalUrl | string | New destination (validated like /shorten; deep links allowed) |
interstitialEnabled | boolean | Show your branded interstitial page before redirecting |
Deactivate a link. The redirect stops working; generated codes are never reassigned to anyone else.
Reusable device-routing templates. A template only defines which device slots exist:
Android, IOS, Mobile, Tablet, Desktop,
Default. The concrete destinations are set per link when you bind it.
OS-specific slots win over device classes, and Default catches everything else.
{
"name": "app-store-router",
"description": "Phones to the app, everyone else to the website",
"rules": [
{ "deviceType": "IOS" },
{ "deviceType": "Android" },
{ "deviceType": "Default" }
]
}
Rename: { "newName": "app-router-v2" }
Delete a template. Bound links fall back to their original destination, and the name can be used again.
Reusable split tests. Define variant names and traffic percentages (they must sum to 100). Each click is bucketed independently. Destinations are set per link when you bind it.
{
"name": "homepage-test",
"description": "New homepage vs. current",
"variants": [
{ "variantName": "Control", "splitPercentage": 50 },
{ "variantName": "New-Design", "splitPercentage": 50 }
]
}
Rename ({ "newName": "..." }) or delete a template. Deleting frees the name for reuse. Variant names can't contain :.
Attach one template to a link and supply a concrete destination for each slot. A link has at most one binding.
The current binding, or { "bound": false }. templateName is the bound template
(null if it was deleted), and chained endpoints include routeTemplateName.
| Field | Type | Required | Description |
|---|---|---|---|
templateType | string | Yes | Device or AbTest |
templateName | string | Yes | Name of your route or A/B template |
endpoints[].slotKey | string | Yes | A device slot (e.g. IOS, Default) or a variant name |
endpoints[].targetUrl | string | Yes | Destination for the slot. May be an app deep link (custom scheme). |
endpoints[].fallbackUrl | string | No | Web fallback when targetUrl is a deep link and the app isn't installed |
endpoints[].targetType | string | No | Url (default), or RouteTemplate to chain an A/B variant into device routing |
endpoints[].routeTemplateName | string | With RouteTemplate | The route template the variant chains to |
Device routing with deep links and store fallbacks:
{
"templateType": "Device",
"templateName": "app-store-router",
"endpoints": [
{ "slotKey": "IOS", "targetUrl": "myapp://product/42", "fallbackUrl": "https://apps.apple.com/app/example/id123456" },
{ "slotKey": "Android", "targetUrl": "myapp://product/42", "fallbackUrl": "https://play.google.com/store/apps/details?id=com.example" },
{ "slotKey": "Default", "targetUrl": "https://example.com/product/42" }
]
}
A/B split:
{
"templateType": "AbTest",
"templateName": "homepage-test",
"endpoints": [
{ "slotKey": "Control", "targetUrl": "https://example.com" },
{ "slotKey": "New-Design", "targetUrl": "https://example.com/v2" }
]
}
A/B split where one variant also routes by device. Give the variant "targetType": "RouteTemplate"
and add its per-device destinations as endpoints keyed Variant:Device. Include every device slot
of the route template, or a Variant:Default:
{
"templateType": "AbTest",
"templateName": "homepage-test",
"endpoints": [
{ "slotKey": "Control", "targetType": "RouteTemplate", "routeTemplateName": "app-store-router" },
{ "slotKey": "Control:IOS", "targetUrl": "https://apps.apple.com/app/example/id123456" },
{ "slotKey": "Control:Default", "targetUrl": "https://example.com" },
{ "slotKey": "New-Design", "targetUrl": "https://example.com/v2" }
]
}
Remove the binding. The link returns to its original destination.
One-off device or A/B rules stored on a single link. Prefer templates and bindings for anything reusable.
A link's binding takes precedence over its per-URL rules. GET also reports the binding.
{
"rules": [
{ "ruleType": "Device", "deviceType": "Mobile", "targetUrl": "https://m.example.com" },
{ "ruleType": "Device", "deviceType": "Desktop", "targetUrl": "https://example.com" }
]
}
Device rules use Mobile, Tablet, or Desktop. A/B rules use
"ruleType": "AbTest" with variantName and splitPercentage (summing to 100).
POST replaces the link's rules. PUT updates one rule's targetUrl,
splitPercentage, or active; active A/B splits must still total 100, so to change
several splits at once, POST the full set.