{
  "openapi": "3.1.0",
  "info": {
    "title": "Gajo API",
    "version": "1.0",
    "summary": "Short links, QR codes, click analytics, device routing, A/B split links and app deep links.",
    "description": "[Gajo](https://gajo.io) is a URL shortener with QR codes, click analytics, device-based routing, A/B split links, and app deep links with an App Store / Google Play fallback. This REST API creates and manages short links and returns their click analytics.\n\n**Every Gajo account plan includes the core API, including Free (no credit card required).** Create an account, generate a key on the [API Keys](https://gajo.io/account/keys) page, and send it in the `x-api-key` header on every request.\n\n## Plans\n\n| | Free | Basic | Pro |\n|---|---|---|---|\n| Price | $0, no card | $4.99 / month | $9.99 / month |\n| New URLs per month | 50 | 500 | 10,000 |\n| API requests per minute | 10 | 30 | 60 |\n| Max link lifetime | 60 days | 120 days | Unlimited |\n| Generated short code length | 6 characters | 5 characters | 5 characters |\n| `GET /urls/{code}/stats` | Click totals + 7-day history | Full analytics: 30-day history, referrers, devices, countries, regions | Full analytics |\n| Batch shortening, editing destinations, routing rules, route & A/B templates, bindings, app deep links | — | — | Yes |\n\nOperations marked `x-gajo-plan: Pro` return `403` with a message such as `\"A Pro plan is required for batch shortening\"` on Free and Basic. The monthly URL allowance is shared with links created in the dashboard. Full plan details: [gajo.io/pricing](https://gajo.io/pricing).\n\n## Responses\n\nEvery response uses the same JSON envelope: `{\"success\": true|false, \"message\": string|null, \"data\": ...}`. On errors `success` is `false` and `message` explains why.\n\n## Rate limits\n\nEach API key may make the plan's number of requests per 60-second sliding window. Responses carry `X-RateLimit-Limit` and `X-RateLimit-Remaining`; over the limit the API returns `429` with a `Retry-After` header (seconds). `POST /shorten` also returns `429` once the monthly URL allowance is used up.\n\nGuides and examples: [gajo.io/docs](https://gajo.io/docs).\n",
    "termsOfService": "https://gajo.io/terms",
    "contact": {
      "name": "Gajo",
      "url": "https://gajo.io"
    }
  },
  "externalDocs": {
    "description": "Gajo API documentation",
    "url": "https://gajo.io/docs"
  },
  "servers": [
    {
      "url": "https://gajo.io/api/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "tags": [
    {
      "name": "Account",
      "description": "Your plan, capabilities and usage. Available on every plan."
    },
    {
      "name": "URLs",
      "description": "Create, list, edit and deactivate short links. Batch shortening and editing destinations require Pro."
    },
    {
      "name": "Analytics",
      "description": "Click analytics per link. Free gets totals and 7-day history; Basic and Pro get full analytics."
    },
    {
      "name": "Routing Rules",
      "description": "Pro. One-off device or A/B rules stored on a single link. For reusable setups use route/A/B templates with bindings; a link's binding takes precedence over its per-URL rules."
    },
    {
      "name": "Route Templates",
      "description": "Pro. Reusable, abstract device-routing templates (device slots only, no URLs)."
    },
    {
      "name": "A/B Templates",
      "description": "Pro. Reusable, abstract split tests (variant names and percentages only, no URLs)."
    },
    {
      "name": "Bindings",
      "description": "Pro. Attach a route or A/B template to a link and supply the concrete destination for each slot, including app deep links with store fallbacks."
    }
  ],
  "paths": {
    "/me": {
      "get": {
        "operationId": "getMe",
        "tags": [
          "Account"
        ],
        "summary": "Get your account",
        "description": "Returns your plan and its capabilities. A cheap way to check that an API key works.",
        "responses": {
          "200": {
            "description": "Your account.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MeResult"
                },
                "example": {
                  "success": true,
                  "message": null,
                  "data": {
                    "plan": "Free",
                    "email": "you@example.com",
                    "urlCount": 12,
                    "apiAccess": true,
                    "customCodes": false,
                    "routingRules": false,
                    "memberSince": "2026-03-22T12:00:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/usage": {
      "get": {
        "operationId": "getUsage",
        "tags": [
          "Account"
        ],
        "summary": "Get usage and limits",
        "description": "This month's link creation against your plan's allowance, plus your API rate limit and maximum link lifetime. The monthly allowance is shared with links created in the dashboard.",
        "responses": {
          "200": {
            "description": "Usage for the current calendar month.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsageResult"
                },
                "examples": {
                  "free": {
                    "summary": "Free plan",
                    "value": {
                      "success": true,
                      "message": null,
                      "data": {
                        "plan": "Free",
                        "urlsCreatedThisMonth": 8,
                        "urlsMonthlyLimit": 50,
                        "urlsRemaining": 42,
                        "requestsPerMinute": 10,
                        "maxLinkLifetimeDays": 60,
                        "totalUrls": 12,
                        "totalClicks": 318
                      }
                    }
                  },
                  "pro": {
                    "summary": "Pro plan (unlimited lifetime)",
                    "value": {
                      "success": true,
                      "message": null,
                      "data": {
                        "plan": "Pro",
                        "urlsCreatedThisMonth": 1240,
                        "urlsMonthlyLimit": 10000,
                        "urlsRemaining": 8760,
                        "requestsPerMinute": 60,
                        "maxLinkLifetimeDays": null,
                        "totalUrls": 5321,
                        "totalClicks": 182904
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/shorten": {
      "post": {
        "operationId": "shortenUrl",
        "tags": [
          "URLs"
        ],
        "summary": "Shorten a URL",
        "description": "Creates a short link on your plan: code length, maximum lifetime and deep-link support follow the plan. `lifetimeDays` is clamped to the plan maximum (60 days on Free, 120 on Basic, unlimited on Pro); omit it to get the maximum. `fallbackUrl` is only valid when `url` is an app deep link (custom scheme such as `myapp://...`), and custom schemes require a Pro plan. Each call counts against your monthly URL allowance; once it is used up the API returns `429`. With `customCode` (Pro), the link uses your custom code and redirects at `https://gajo.io/c/{code}`: an unclaimed code is claimed for you, and one of your own codes is used if it isn't currently linked to a URL. A code that's taken, released, or already linked returns `409` (change a linked code's destination with `PUT /urls/{code}`). Custom-code links don't expire, so `lifetimeDays` can't be combined with `customCode`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ShortenRequest"
              },
              "examples": {
                "default": {
                  "summary": "Shorten with the plan's default lifetime",
                  "value": {
                    "url": "https://example.com/blog/launch-announcement"
                  }
                },
                "withLifetime": {
                  "summary": "Expire after 30 days",
                  "value": {
                    "url": "https://example.com/spring-sale",
                    "lifetimeDays": 30
                  }
                },
                "deepLink": {
                  "summary": "Pro: app deep link with App Store fallback",
                  "value": {
                    "url": "myapp://product/42",
                    "fallbackUrl": "https://apps.apple.com/app/example/id123456789"
                  }
                },
                "customCode": {
                  "summary": "Pro: use a custom code (redirects at /c/launch)",
                  "value": {
                    "url": "https://example.com/blog/launch-announcement",
                    "customCode": "launch"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new short link. Share it as `https://gajo.io/{shortCode}`.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShortUrlResult"
                },
                "examples": {
                  "default": {
                    "summary": "Free plan, default lifetime (60 days)",
                    "value": {
                      "success": true,
                      "message": "URL shortened successfully",
                      "data": {
                        "shortCode": "Xk4mPq",
                        "originalUrl": "https://example.com/blog/launch-announcement",
                        "fallbackUrl": null,
                        "active": true,
                        "clickCount": 0,
                        "isCustomCode": false,
                        "createdDate": "2026-10-07T14:03:11.482Z",
                        "expiresDate": "2026-12-06T14:03:11.482Z"
                      }
                    }
                  },
                  "withLifetime": {
                    "summary": "30-day link",
                    "value": {
                      "success": true,
                      "message": "URL shortened successfully",
                      "data": {
                        "shortCode": "Rt8wZe",
                        "originalUrl": "https://example.com/spring-sale",
                        "fallbackUrl": null,
                        "active": true,
                        "clickCount": 0,
                        "isCustomCode": false,
                        "createdDate": "2026-10-07T14:03:11.482Z",
                        "expiresDate": "2026-11-06T14:03:11.482Z"
                      }
                    }
                  },
                  "deepLink": {
                    "summary": "Pro: app deep link with App Store fallback",
                    "value": {
                      "success": true,
                      "message": "URL shortened successfully",
                      "data": {
                        "shortCode": "Hn7Kp",
                        "originalUrl": "myapp://product/42",
                        "fallbackUrl": "https://apps.apple.com/app/example/id123456789",
                        "active": true,
                        "clickCount": 0,
                        "isCustomCode": false,
                        "createdDate": "2026-10-07T14:03:11.482Z",
                        "expiresDate": null
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request. `message` explains what to fix.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "examples": {
                  "unsupportedScheme": {
                    "summary": "Unsupported scheme (custom schemes need Pro)",
                    "value": {
                      "success": false,
                      "message": "URL must use a supported scheme (http, https, mailto, tel, ftp, sftp)",
                      "data": null
                    }
                  },
                  "badLifetime": {
                    "summary": "lifetimeDays below 1",
                    "value": {
                      "success": false,
                      "message": "lifetimeDays must be at least 1",
                      "data": null
                    }
                  },
                  "fallbackWithoutDeepLink": {
                    "summary": "fallbackUrl with a web URL",
                    "value": {
                      "success": false,
                      "message": "A fallback URL is only allowed when the destination is an app deep link",
                      "data": null
                    }
                  },
                  "selfLink": {
                    "summary": "Destination on gajo.io",
                    "value": {
                      "success": false,
                      "message": "Cannot shorten URLs pointing to gajo.io",
                      "data": null
                    }
                  },
                  "invalidJson": {
                    "summary": "Malformed body or missing required field",
                    "value": {
                      "success": false,
                      "message": "Invalid JSON format in request. Missing url or required field.",
                      "data": null
                    }
                  },
                  "customCodeWithLifetime": {
                    "summary": "lifetimeDays sent with customCode",
                    "value": {
                      "success": false,
                      "message": "lifetimeDays can't be combined with customCode: custom-code links don't expire",
                      "data": null
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`customCode` was sent but your plan is not Pro.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A Pro plan is required for custom codes"
                }
              }
            }
          },
          "409": {
            "description": "The `customCode` can't be used: it belongs to another account, was released, clashes with a generated code, or is already linked to one of your URLs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "Custom code 'launch' already points to a URL; change its destination with PUT /urls/launch"
                }
              }
            }
          },
          "429": {
            "description": "Either the per-minute rate limit was exceeded (`Retry-After` is set), or the plan's monthly URL allowance is used up (no `Retry-After`; wait for the next calendar month or upgrade).",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait. Present only for the per-minute rate limit.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "examples": {
                  "rateLimit": {
                    "summary": "Per-minute rate limit",
                    "value": {
                      "success": false,
                      "message": "Rate limit exceeded. Please try again later."
                    }
                  },
                  "monthlyLimit": {
                    "summary": "Monthly URL allowance used up",
                    "value": {
                      "success": false,
                      "message": "Monthly URL limit reached for your plan. See GET /usage, or upgrade for a higher limit.",
                      "data": null
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/shorten/batch": {
      "post": {
        "operationId": "shortenUrlBatch",
        "tags": [
          "URLs"
        ],
        "summary": "Shorten up to 100 URLs",
        "description": "**Requires a Pro plan.** Creates up to 100 short links in one request. Each item takes the same fields as `POST /shorten` and is processed independently: the response lists a success or an error for every item, in request order, and the request itself still returns `200` when some items fail. Each created link counts against your monthly URL allowance; items beyond the allowance fail individually.",
        "x-gajo-plan": "Pro",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchShortenRequest"
              },
              "example": {
                "urls": [
                  {
                    "url": "https://example.com/page1",
                    "lifetimeDays": 90
                  },
                  {
                    "url": "https://example.com/page2"
                  },
                  {
                    "url": "example.com/no-scheme"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-item results.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchShortenResult"
                },
                "example": {
                  "success": true,
                  "message": "Batch processing complete",
                  "data": [
                    {
                      "success": true,
                      "url": {
                        "shortCode": "aB3dE",
                        "originalUrl": "https://example.com/page1",
                        "fallbackUrl": null,
                        "active": true,
                        "clickCount": 0,
                        "isCustomCode": false,
                        "createdDate": "2026-10-07T14:03:11.482Z",
                        "expiresDate": "2027-01-05T14:03:11.482Z"
                      }
                    },
                    {
                      "success": true,
                      "url": {
                        "shortCode": "Wq9Ts",
                        "originalUrl": "https://example.com/page2",
                        "fallbackUrl": null,
                        "active": true,
                        "clickCount": 0,
                        "isCustomCode": false,
                        "createdDate": "2026-10-07T14:03:11.482Z",
                        "expiresDate": null
                      }
                    },
                    {
                      "success": false,
                      "error": "URL must use a supported scheme (http, https, mailto, tel, ftp, sftp)",
                      "originalUrl": "example.com/no-scheme"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid request. `message` explains what to fix.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "examples": {
                  "tooMany": {
                    "summary": "More than 100 URLs",
                    "value": {
                      "success": false,
                      "message": "A batch can contain at most 100 URLs",
                      "data": null
                    }
                  },
                  "invalidJson": {
                    "summary": "Malformed body or missing required field",
                    "value": {
                      "success": false,
                      "message": "Invalid JSON format in request. Missing url or required field.",
                      "data": null
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Your plan is not Pro (the message names the Pro feature).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A Pro plan is required for batch shortening"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/urls": {
      "get": {
        "operationId": "listUrls",
        "tags": [
          "URLs"
        ],
        "summary": "List your links",
        "description": "Your links, newest first, including deactivated and expired links, one page at a time. `X-Total-Count` has the total; when more links remain, a `Link` header (`rel=\"next\"`) points at the next page.",
        "responses": {
          "200": {
            "description": "Your links.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-Total-Count": {
                "$ref": "#/components/headers/X-Total-Count"
              },
              "Link": {
                "$ref": "#/components/headers/Link"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShortUrlListResult"
                },
                "example": {
                  "success": true,
                  "message": null,
                  "data": [
                    {
                      "shortCode": "Xk4mPq",
                      "originalUrl": "https://example.com/blog/launch-announcement",
                      "fallbackUrl": null,
                      "active": true,
                      "clickCount": 0,
                      "isCustomCode": false,
                      "createdDate": "2026-10-07T14:03:11.482Z",
                      "expiresDate": "2026-12-06T14:03:11.482Z"
                    },
                    {
                      "shortCode": "Fg5vNc",
                      "originalUrl": "https://example.com/docs/getting-started",
                      "fallbackUrl": null,
                      "active": false,
                      "clickCount": 318,
                      "isCustomCode": false,
                      "createdDate": "2026-09-14T08:21:40.117Z",
                      "expiresDate": "2026-11-13T08:21:40.117Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`page` or `limit` is out of range.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "limit must be from 1 to 1000",
                  "data": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            },
            "description": "Page number, starting at 1."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            },
            "description": "Links per page."
          }
        ]
      }
    },
    "/urls/{code}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ShortCode"
        }
      ],
      "get": {
        "operationId": "getUrl",
        "tags": [
          "URLs"
        ],
        "summary": "Get a link",
        "description": "Returns one of your links by short code.",
        "responses": {
          "200": {
            "description": "The link.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShortUrlResult"
                },
                "example": {
                  "success": true,
                  "message": null,
                  "data": {
                    "shortCode": "Xk4mPq",
                    "originalUrl": "https://example.com/blog/launch-announcement",
                    "fallbackUrl": null,
                    "active": true,
                    "clickCount": 0,
                    "isCustomCode": false,
                    "createdDate": "2026-10-07T14:03:11.482Z",
                    "expiresDate": "2026-12-06T14:03:11.482Z"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UrlNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "put": {
        "operationId": "updateUrl",
        "tags": [
          "URLs"
        ],
        "summary": "Edit a link",
        "description": "**Requires a Pro plan.** Changes a link's destination and/or interstitial setting. Send only the fields to change. The new destination takes effect immediately and may be an app deep link (custom scheme).",
        "x-gajo-plan": "Pro",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateUrlRequest"
              },
              "examples": {
                "destination": {
                  "summary": "Change the destination",
                  "value": {
                    "originalUrl": "https://example.com/blog/launch-recap"
                  }
                },
                "interstitial": {
                  "summary": "Turn on the interstitial page",
                  "value": {
                    "interstitialEnabled": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated link.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShortUrlResult"
                },
                "example": {
                  "success": true,
                  "message": "URL updated",
                  "data": {
                    "shortCode": "Xk4mPq",
                    "originalUrl": "https://example.com/blog/launch-recap",
                    "fallbackUrl": null,
                    "active": true,
                    "clickCount": 57,
                    "isCustomCode": false,
                    "createdDate": "2026-10-07T14:03:11.482Z",
                    "expiresDate": "2026-12-06T14:03:11.482Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request. `message` explains what to fix.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "examples": {
                  "unsupportedScheme": {
                    "summary": "Invalid destination",
                    "value": {
                      "success": false,
                      "message": "URL must use a supported scheme (http, https, mailto, tel, ftp, sftp)",
                      "data": null
                    }
                  },
                  "badInterstitial": {
                    "summary": "interstitialEnabled not a boolean",
                    "value": {
                      "success": false,
                      "message": "interstitialEnabled must be a boolean",
                      "data": null
                    }
                  },
                  "invalidJson": {
                    "summary": "Malformed body or missing required field",
                    "value": {
                      "success": false,
                      "message": "Invalid JSON format in request. Missing url or required field.",
                      "data": null
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Your plan is not Pro.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "examples": {
                  "proRequired": {
                    "summary": "Not on a Pro plan",
                    "value": {
                      "success": false,
                      "message": "A Pro plan is required for editing link destinations"
                    }
                  },
                  "notAuthorized": {
                    "summary": "The link belongs to another account",
                    "value": {
                      "success": false,
                      "message": "Not authorized",
                      "data": null
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/UrlNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "delete": {
        "operationId": "deactivateUrl",
        "tags": [
          "URLs"
        ],
        "summary": "Deactivate a link",
        "description": "Deactivates a link so it stops redirecting. The link stays in `GET /urls` with `active: false`. Generated short codes are never reassigned to anyone else.",
        "responses": {
          "200": {
            "description": "The link was deactivated.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResult"
                },
                "example": {
                  "success": true,
                  "message": "URL deactivated",
                  "data": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UrlNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/urls/{code}/stats": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ShortCode"
        }
      ],
      "get": {
        "operationId": "getUrlStats",
        "tags": [
          "Analytics"
        ],
        "summary": "Get click analytics for a link",
        "description": "Click analytics for one of your links; bot and link-preview traffic is excluded. Free plans get `totalClicks`, `historyDays` (7) and 7 days of `clicksByDay`. Basic and Pro get 30 days of history plus `topReferrers`, `deviceBreakdown`, `topCountries` and `topRegions`; those four fields are only present on Basic and Pro.",
        "responses": {
          "200": {
            "description": "Click analytics.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UrlStatsResult"
                },
                "examples": {
                  "free": {
                    "summary": "Free plan",
                    "value": {
                      "success": true,
                      "message": null,
                      "data": {
                        "totalClicks": 318,
                        "historyDays": 7,
                        "clicksByDay": [
                          {
                            "date": "2026-10-01",
                            "clicks": 29
                          },
                          {
                            "date": "2026-10-02",
                            "clicks": 44
                          },
                          {
                            "date": "2026-10-04",
                            "clicks": 37
                          },
                          {
                            "date": "2026-10-06",
                            "clicks": 42
                          }
                        ]
                      }
                    }
                  },
                  "full": {
                    "summary": "Basic or Pro plan",
                    "value": {
                      "success": true,
                      "message": null,
                      "data": {
                        "totalClicks": 318,
                        "historyDays": 30,
                        "clicksByDay": [
                          {
                            "date": "2026-09-28",
                            "clicks": 12
                          },
                          {
                            "date": "2026-10-01",
                            "clicks": 29
                          },
                          {
                            "date": "2026-10-06",
                            "clicks": 42
                          }
                        ],
                        "topReferrers": [
                          {
                            "source": "https://news.ycombinator.com/",
                            "count": 120
                          },
                          {
                            "source": "Direct",
                            "count": 88
                          }
                        ],
                        "deviceBreakdown": [
                          {
                            "device": "Mobile",
                            "percentage": 61.2
                          },
                          {
                            "device": "Desktop",
                            "percentage": 33.5
                          },
                          {
                            "device": "Tablet",
                            "percentage": 5.3
                          }
                        ],
                        "topCountries": [
                          {
                            "country": "United States",
                            "count": 140
                          },
                          {
                            "country": "Germany",
                            "count": 37
                          }
                        ],
                        "topRegions": [
                          {
                            "region": "California, US",
                            "count": 51
                          },
                          {
                            "region": "Bavaria, DE",
                            "count": 14
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UrlNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/urls/{code}/routing": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ShortCode"
        }
      ],
      "get": {
        "operationId": "getRoutingRules",
        "tags": [
          "Routing Rules"
        ],
        "summary": "Get a link's routing",
        "description": "**Requires a Pro plan.** Returns the link's per-URL routing rules (ordered by priority) and, if present, its active template binding. At redirect time a binding takes precedence over per-URL rules.",
        "x-gajo-plan": "Pro",
        "responses": {
          "200": {
            "description": "Routing rules and binding.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoutingOverviewResult"
                },
                "example": {
                  "success": true,
                  "message": null,
                  "data": {
                    "shortCode": "Hn7Kp",
                    "rules": [
                      {
                        "id": 101,
                        "ruleType": "Device",
                        "priority": 0,
                        "active": true,
                        "deviceType": "Mobile",
                        "targetUrl": "https://m.example.com",
                        "variantName": null,
                        "splitPercentage": null,
                        "createdDate": "2026-10-07T14:03:11.482Z"
                      },
                      {
                        "id": 102,
                        "ruleType": "Device",
                        "priority": 1,
                        "active": true,
                        "deviceType": "Desktop",
                        "targetUrl": "https://example.com",
                        "variantName": null,
                        "splitPercentage": null,
                        "createdDate": "2026-10-07T14:03:11.482Z"
                      }
                    ],
                    "binding": null
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Your plan is not Pro (the message names the Pro feature).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A Pro plan is required for routing rules"
                }
              }
            }
          },
          "404": {
            "description": "No link with this code exists on your account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "URL not found",
                  "data": null
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "post": {
        "operationId": "createRoutingRules",
        "tags": [
          "Routing Rules"
        ],
        "summary": "Set a link's routing rules",
        "description": "**Requires a Pro plan.** Sets one-off device or A/B rules on a single link, replacing any rules it already has. All rules in a request must share one `ruleType`. `Device` rules take a `deviceType` of `Mobile`, `Tablet` or `Desktop` (each at most once). `AbTest` rules need a `variantName` and a `splitPercentage` of at least 1, summing to 100. For reusable setups, prefer route/A/B templates and bindings.",
        "x-gajo-plan": "Pro",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateRoutingRulesRequest"
              },
              "examples": {
                "device": {
                  "summary": "Route by device class",
                  "value": {
                    "rules": [
                      {
                        "ruleType": "Device",
                        "deviceType": "Mobile",
                        "targetUrl": "https://m.example.com"
                      },
                      {
                        "ruleType": "Device",
                        "deviceType": "Desktop",
                        "targetUrl": "https://example.com"
                      }
                    ]
                  }
                },
                "abTest": {
                  "summary": "50/50 split",
                  "value": {
                    "rules": [
                      {
                        "ruleType": "AbTest",
                        "variantName": "A",
                        "splitPercentage": 50,
                        "targetUrl": "https://example.com/landing-a"
                      },
                      {
                        "ruleType": "AbTest",
                        "variantName": "B",
                        "splitPercentage": 50,
                        "targetUrl": "https://example.com/landing-b"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The rules that were created.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoutingRulesCreatedResult"
                },
                "examples": {
                  "device": {
                    "summary": "Device rules",
                    "value": {
                      "success": true,
                      "message": "Routing rules saved",
                      "data": {
                        "shortCode": "Hn7Kp",
                        "ruleType": "Device",
                        "rules": [
                          {
                            "id": 101,
                            "ruleType": "Device",
                            "priority": 0,
                            "active": true,
                            "deviceType": "Mobile",
                            "targetUrl": "https://m.example.com",
                            "variantName": null,
                            "splitPercentage": null,
                            "createdDate": "2026-10-07T14:03:11.482Z"
                          },
                          {
                            "id": 102,
                            "ruleType": "Device",
                            "priority": 1,
                            "active": true,
                            "deviceType": "Desktop",
                            "targetUrl": "https://example.com",
                            "variantName": null,
                            "splitPercentage": null,
                            "createdDate": "2026-10-07T14:03:11.482Z"
                          }
                        ]
                      }
                    }
                  },
                  "abTest": {
                    "summary": "A/B rules",
                    "value": {
                      "success": true,
                      "message": "Routing rules saved",
                      "data": {
                        "shortCode": "Hn7Kp",
                        "ruleType": "AbTest",
                        "rules": [
                          {
                            "id": 201,
                            "ruleType": "AbTest",
                            "priority": 0,
                            "active": true,
                            "deviceType": null,
                            "targetUrl": "https://example.com/landing-a",
                            "variantName": "A",
                            "splitPercentage": 50,
                            "createdDate": "2026-10-07T14:03:11.482Z"
                          },
                          {
                            "id": 202,
                            "ruleType": "AbTest",
                            "priority": 1,
                            "active": true,
                            "deviceType": null,
                            "targetUrl": "https://example.com/landing-b",
                            "variantName": "B",
                            "splitPercentage": 50,
                            "createdDate": "2026-10-07T14:03:11.482Z"
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request. `message` explains what to fix.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "examples": {
                  "noRules": {
                    "summary": "Empty rules array",
                    "value": {
                      "success": false,
                      "message": "At least one rule is required",
                      "data": null
                    }
                  },
                  "badDeviceType": {
                    "summary": "Unsupported deviceType",
                    "value": {
                      "success": false,
                      "message": "Invalid deviceType: Phone. Must be Mobile, Tablet, or Desktop.",
                      "data": null
                    }
                  },
                  "badSplit": {
                    "summary": "Percentages do not sum to 100",
                    "value": {
                      "success": false,
                      "message": "A/B test split percentages must sum to 100 (currently: 90)",
                      "data": null
                    }
                  },
                  "typeConflict": {
                    "summary": "Link already has rules of the other type",
                    "value": {
                      "success": false,
                      "message": "URL already has Device rules. Delete them first to switch types.",
                      "data": null
                    }
                  },
                  "badTarget": {
                    "summary": "Invalid target URL",
                    "value": {
                      "success": false,
                      "message": "Invalid target URL: URL must use a supported scheme (http, https, mailto, tel, ftp, sftp)",
                      "data": null
                    }
                  },
                  "invalidJson": {
                    "summary": "Malformed body or missing required field",
                    "value": {
                      "success": false,
                      "message": "Invalid JSON format in request. Missing url or required field.",
                      "data": null
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Your plan is not Pro (the message names the Pro feature).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A Pro plan is required for routing rules"
                }
              }
            }
          },
          "404": {
            "description": "No link with this code exists on your account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "URL not found",
                  "data": null
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "delete": {
        "operationId": "deleteAllRoutingRules",
        "tags": [
          "Routing Rules"
        ],
        "summary": "Delete all routing rules for a link",
        "description": "**Requires a Pro plan.** Removes every per-URL routing rule from the link. A template binding, if any, is not affected.",
        "x-gajo-plan": "Pro",
        "responses": {
          "200": {
            "description": "All rules were deleted.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResult"
                },
                "example": {
                  "success": true,
                  "message": "All routing rules deleted",
                  "data": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Your plan is not Pro (the message names the Pro feature).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A Pro plan is required for routing rules"
                }
              }
            }
          },
          "404": {
            "description": "No link with this code exists on your account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "URL not found",
                  "data": null
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/urls/{code}/routing/{ruleId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ShortCode"
        },
        {
          "$ref": "#/components/parameters/RuleId"
        }
      ],
      "put": {
        "operationId": "updateRoutingRule",
        "tags": [
          "Routing Rules"
        ],
        "summary": "Update a routing rule",
        "description": "**Requires a Pro plan.** Changes a rule's `targetUrl`, `splitPercentage` or `active` flag. Send only the fields to change; values of the wrong type are rejected with `400`. `splitPercentage` only applies to `AbTest` rules, and the link's active A/B splits must still total 100 afterwards, so to change several splits at once, replace the rules with `POST /urls/{code}/routing`. Changes apply to new clicks immediately.",
        "x-gajo-plan": "Pro",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateRoutingRuleRequest"
              },
              "examples": {
                "target": {
                  "summary": "Change the destination",
                  "value": {
                    "targetUrl": "https://m.example.com/v2"
                  }
                },
                "pause": {
                  "summary": "Pause the rule",
                  "value": {
                    "active": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated rule.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoutingRuleResult"
                },
                "example": {
                  "success": true,
                  "message": "Routing rule updated",
                  "data": {
                    "id": 101,
                    "ruleType": "Device",
                    "priority": 0,
                    "active": true,
                    "deviceType": "Mobile",
                    "targetUrl": "https://m.example.com/v2",
                    "variantName": null,
                    "splitPercentage": null,
                    "createdDate": "2026-10-07T14:03:11.482Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request. `message` explains what to fix.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "examples": {
                  "badTarget": {
                    "summary": "Invalid target URL",
                    "value": {
                      "success": false,
                      "message": "Invalid target URL: URL must have a valid host",
                      "data": null
                    }
                  },
                  "badRuleId": {
                    "summary": "Non-numeric rule ID",
                    "value": {
                      "success": false,
                      "message": "Invalid value for parameter: ruleId",
                      "data": null
                    }
                  },
                  "invalidJson": {
                    "summary": "Malformed body or missing required field",
                    "value": {
                      "success": false,
                      "message": "Invalid JSON format in request. Missing url or required field.",
                      "data": null
                    }
                  },
                  "splitTotal": {
                    "summary": "Active A/B splits would not total 100",
                    "value": {
                      "success": false,
                      "message": "Active A/B split percentages must sum to 100 (this change makes 120). To change several splits at once, replace the rules with POST /urls/abc12/routing.",
                      "data": null
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "The link or the rule was not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "examples": {
                  "urlNotFound": {
                    "summary": "Unknown short code",
                    "value": {
                      "success": false,
                      "message": "URL not found",
                      "data": null
                    }
                  },
                  "ruleNotFound": {
                    "summary": "Unknown rule ID",
                    "value": {
                      "success": false,
                      "message": "Routing rule not found",
                      "data": null
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "delete": {
        "operationId": "deleteRoutingRule",
        "tags": [
          "Routing Rules"
        ],
        "summary": "Delete a routing rule",
        "description": "**Requires a Pro plan.** Removes one per-URL routing rule.",
        "x-gajo-plan": "Pro",
        "responses": {
          "200": {
            "description": "The rule was deleted.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResult"
                },
                "example": {
                  "success": true,
                  "message": "Routing rule deleted",
                  "data": null
                }
              }
            }
          },
          "400": {
            "description": "The rule ID is not a number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "Invalid value for parameter: ruleId",
                  "data": null
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "The link or the rule was not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "examples": {
                  "urlNotFound": {
                    "summary": "Unknown short code",
                    "value": {
                      "success": false,
                      "message": "URL not found",
                      "data": null
                    }
                  },
                  "ruleNotFound": {
                    "summary": "Unknown rule ID",
                    "value": {
                      "success": false,
                      "message": "Routing rule not found",
                      "data": null
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/route-templates": {
      "get": {
        "operationId": "listRouteTemplates",
        "tags": [
          "Route Templates"
        ],
        "summary": "List route templates",
        "description": "**Requires a Pro plan.** Returns your active route templates. Route templates are abstract: they list device slots but contain no URLs. Concrete destinations are set per link with `POST /urls/{code}/binding`.",
        "x-gajo-plan": "Pro",
        "responses": {
          "200": {
            "description": "Your route templates.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RouteTemplateListResult"
                },
                "example": {
                  "success": true,
                  "message": null,
                  "data": [
                    {
                      "name": "app-store-router",
                      "description": "Phones to the app, everyone else to the website",
                      "active": true,
                      "createdDate": "2026-10-07T14:03:11.482Z",
                      "updatedDate": null,
                      "rules": [
                        {
                          "deviceType": "IOS",
                          "priority": 0
                        },
                        {
                          "deviceType": "Android",
                          "priority": 1
                        },
                        {
                          "deviceType": "Default",
                          "priority": 2
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Your plan is not Pro (the message names the Pro feature).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A Pro plan is required for route templates"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "post": {
        "operationId": "createRouteTemplate",
        "tags": [
          "Route Templates"
        ],
        "summary": "Create a route template",
        "description": "**Requires a Pro plan.** Creates a reusable device-routing template from a list of device slots (`Android`, `IOS`, `Mobile`, `Tablet`, `Desktop`, `Default`), each at most once. At redirect time OS-specific slots win over device classes, and `Default` catches everything else. Route templates are abstract: they list device slots but contain no URLs. Concrete destinations are set per link with `POST /urls/{code}/binding`.",
        "x-gajo-plan": "Pro",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RouteTemplateRequest"
              },
              "example": {
                "name": "app-store-router",
                "description": "Phones to the app, everyone else to the website",
                "rules": [
                  {
                    "deviceType": "IOS"
                  },
                  {
                    "deviceType": "Android"
                  },
                  {
                    "deviceType": "Default"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new template.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RouteTemplateResult"
                },
                "example": {
                  "success": true,
                  "message": "Route template created",
                  "data": {
                    "name": "app-store-router",
                    "description": "Phones to the app, everyone else to the website",
                    "active": true,
                    "createdDate": "2026-10-07T14:03:11.482Z",
                    "updatedDate": null,
                    "rules": [
                      {
                        "deviceType": "IOS",
                        "priority": 0
                      },
                      {
                        "deviceType": "Android",
                        "priority": 1
                      },
                      {
                        "deviceType": "Default",
                        "priority": 2
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request. `message` explains what to fix.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "examples": {
                  "badName": {
                    "summary": "Invalid template name",
                    "value": {
                      "success": false,
                      "message": "Template name must be 2-100 characters, alphanumeric, hyphens, and underscores only",
                      "data": null
                    }
                  },
                  "noRules": {
                    "summary": "Empty rules array",
                    "value": {
                      "success": false,
                      "message": "At least one device rule is required",
                      "data": null
                    }
                  },
                  "duplicate": {
                    "summary": "Repeated deviceType",
                    "value": {
                      "success": false,
                      "message": "Duplicate device types are not allowed",
                      "data": null
                    }
                  },
                  "badDeviceType": {
                    "summary": "Unknown deviceType",
                    "value": {
                      "success": false,
                      "message": "Invalid device type: Phone. Must be one of: Android, IOS, Mobile, Tablet, Desktop, Default",
                      "data": null
                    }
                  },
                  "exists": {
                    "summary": "Name already used",
                    "value": {
                      "success": false,
                      "message": "A route template named 'app-store-router' already exists",
                      "data": null
                    }
                  },
                  "invalidJson": {
                    "summary": "Malformed body or missing required field",
                    "value": {
                      "success": false,
                      "message": "Invalid JSON format in request. Missing url or required field.",
                      "data": null
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Your plan is not Pro (the message names the Pro feature).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A Pro plan is required for route templates"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/route-templates/{name}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/TemplateName"
        }
      ],
      "get": {
        "operationId": "getRouteTemplate",
        "tags": [
          "Route Templates"
        ],
        "summary": "Get a route template",
        "description": "**Requires a Pro plan.** Returns one route template by name.",
        "x-gajo-plan": "Pro",
        "responses": {
          "200": {
            "description": "The template.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RouteTemplateResult"
                },
                "example": {
                  "success": true,
                  "message": null,
                  "data": {
                    "name": "app-store-router",
                    "description": "Phones to the app, everyone else to the website",
                    "active": true,
                    "createdDate": "2026-10-07T14:03:11.482Z",
                    "updatedDate": null,
                    "rules": [
                      {
                        "deviceType": "IOS",
                        "priority": 0
                      },
                      {
                        "deviceType": "Android",
                        "priority": 1
                      },
                      {
                        "deviceType": "Default",
                        "priority": 2
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Your plan is not Pro (the message names the Pro feature).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A Pro plan is required for route templates"
                }
              }
            }
          },
          "404": {
            "description": "No route template with this name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "Route template not found",
                  "data": null
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "put": {
        "operationId": "updateRouteTemplate",
        "tags": [
          "Route Templates"
        ],
        "summary": "Replace a route template's rules",
        "description": "**Requires a Pro plan.** Replaces the template's description and full list of device slots. The body uses the same shape as create; its `name` must be present but is not used to rename (use `PATCH`). Omitting `description` clears it. Links bound to the template pick up the change immediately.",
        "x-gajo-plan": "Pro",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RouteTemplateRequest"
              },
              "example": {
                "name": "app-store-router",
                "description": "Phones to the app, everyone else to the website",
                "rules": [
                  {
                    "deviceType": "IOS"
                  },
                  {
                    "deviceType": "Android"
                  },
                  {
                    "deviceType": "Tablet"
                  },
                  {
                    "deviceType": "Default"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated template.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RouteTemplateResult"
                },
                "example": {
                  "success": true,
                  "message": "Route template updated",
                  "data": {
                    "name": "app-store-router",
                    "description": "Phones to the app, everyone else to the website",
                    "active": true,
                    "createdDate": "2026-10-07T14:03:11.482Z",
                    "updatedDate": "2026-10-08T09:15:00.250Z",
                    "rules": [
                      {
                        "deviceType": "IOS",
                        "priority": 0
                      },
                      {
                        "deviceType": "Android",
                        "priority": 1
                      },
                      {
                        "deviceType": "Tablet",
                        "priority": 2
                      },
                      {
                        "deviceType": "Default",
                        "priority": 3
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request. `message` explains what to fix.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "examples": {
                  "noRules": {
                    "summary": "Empty rules array",
                    "value": {
                      "success": false,
                      "message": "At least one device rule is required",
                      "data": null
                    }
                  },
                  "duplicate": {
                    "summary": "Repeated deviceType",
                    "value": {
                      "success": false,
                      "message": "Duplicate device types are not allowed",
                      "data": null
                    }
                  },
                  "badDeviceType": {
                    "summary": "Unknown deviceType",
                    "value": {
                      "success": false,
                      "message": "Invalid device type: Phone. Must be one of: Android, IOS, Mobile, Tablet, Desktop, Default",
                      "data": null
                    }
                  },
                  "invalidJson": {
                    "summary": "Malformed body or missing required field",
                    "value": {
                      "success": false,
                      "message": "Invalid JSON format in request. Missing url or required field.",
                      "data": null
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Your plan is not Pro (the message names the Pro feature).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A Pro plan is required for route templates"
                }
              }
            }
          },
          "404": {
            "description": "No route template with this name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "Route template 'app-store-router' not found",
                  "data": null
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "patch": {
        "operationId": "renameRouteTemplate",
        "tags": [
          "Route Templates"
        ],
        "summary": "Rename a route template",
        "description": "**Requires a Pro plan.** Renames the template. Existing bindings keep working.",
        "x-gajo-plan": "Pro",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RenameTemplateRequest"
              },
              "example": {
                "newName": "app-router-v2"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The renamed template.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RouteTemplateResult"
                },
                "example": {
                  "success": true,
                  "message": "Route template renamed",
                  "data": {
                    "name": "app-router-v2",
                    "description": "Phones to the app, everyone else to the website",
                    "active": true,
                    "createdDate": "2026-10-07T14:03:11.482Z",
                    "updatedDate": "2026-10-08T09:15:00.250Z",
                    "rules": [
                      {
                        "deviceType": "IOS",
                        "priority": 0
                      },
                      {
                        "deviceType": "Android",
                        "priority": 1
                      },
                      {
                        "deviceType": "Default",
                        "priority": 2
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request. `message` explains what to fix.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "examples": {
                  "badName": {
                    "summary": "Invalid template name",
                    "value": {
                      "success": false,
                      "message": "Template name must be 2-100 characters, alphanumeric, hyphens, and underscores only",
                      "data": null
                    }
                  },
                  "exists": {
                    "summary": "Name already used",
                    "value": {
                      "success": false,
                      "message": "A route template named 'app-router-v2' already exists",
                      "data": null
                    }
                  },
                  "invalidJson": {
                    "summary": "Malformed body or missing required field",
                    "value": {
                      "success": false,
                      "message": "Invalid JSON format in request. Missing url or required field.",
                      "data": null
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Your plan is not Pro (the message names the Pro feature).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A Pro plan is required for route templates"
                }
              }
            }
          },
          "404": {
            "description": "No route template with this name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "Route template 'app-store-router' not found",
                  "data": null
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "delete": {
        "operationId": "deleteRouteTemplate",
        "tags": [
          "Route Templates"
        ],
        "summary": "Delete a route template",
        "description": "**Requires a Pro plan.** Deletes the template. Links bound to it stop routing through it and fall back to their original destination. The name can be used again right away.",
        "x-gajo-plan": "Pro",
        "responses": {
          "200": {
            "description": "The template was deleted.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResult"
                },
                "example": {
                  "success": true,
                  "message": "Route template deleted",
                  "data": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Your plan is not Pro (the message names the Pro feature).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A Pro plan is required for route templates"
                }
              }
            }
          },
          "404": {
            "description": "No route template with this name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "Route template 'app-store-router' not found",
                  "data": null
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/ab-templates": {
      "get": {
        "operationId": "listAbTemplates",
        "tags": [
          "A/B Templates"
        ],
        "summary": "List A/B templates",
        "description": "**Requires a Pro plan.** Returns your active A/B templates. A/B templates are abstract: variant names and traffic percentages only, no URLs. Concrete destinations are set per link with `POST /urls/{code}/binding`.",
        "x-gajo-plan": "Pro",
        "responses": {
          "200": {
            "description": "Your A/B templates.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AbTemplateListResult"
                },
                "example": {
                  "success": true,
                  "message": null,
                  "data": [
                    {
                      "name": "homepage-test",
                      "description": "New homepage vs. current",
                      "active": true,
                      "createdDate": "2026-10-07T14:03:11.482Z",
                      "updatedDate": null,
                      "variants": [
                        {
                          "variantName": "Control",
                          "splitPercentage": 50,
                          "priority": 0
                        },
                        {
                          "variantName": "New-Design",
                          "splitPercentage": 50,
                          "priority": 1
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Your plan is not Pro (the message names the Pro feature).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A Pro plan is required for A/B templates"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "post": {
        "operationId": "createAbTemplate",
        "tags": [
          "A/B Templates"
        ],
        "summary": "Create an A/B template",
        "description": "**Requires a Pro plan.** Creates a reusable split test. Variant names must be unique and `splitPercentage` values (each at least 1) must sum to 100. Each click is assigned to a variant at random according to the percentages. A/B templates are abstract: variant names and traffic percentages only, no URLs. Concrete destinations are set per link with `POST /urls/{code}/binding`.",
        "x-gajo-plan": "Pro",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AbTemplateRequest"
              },
              "example": {
                "name": "homepage-test",
                "description": "New homepage vs. current",
                "variants": [
                  {
                    "variantName": "Control",
                    "splitPercentage": 50
                  },
                  {
                    "variantName": "New-Design",
                    "splitPercentage": 50
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new template.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AbTemplateResult"
                },
                "example": {
                  "success": true,
                  "message": "A/B template created",
                  "data": {
                    "name": "homepage-test",
                    "description": "New homepage vs. current",
                    "active": true,
                    "createdDate": "2026-10-07T14:03:11.482Z",
                    "updatedDate": null,
                    "variants": [
                      {
                        "variantName": "Control",
                        "splitPercentage": 50,
                        "priority": 0
                      },
                      {
                        "variantName": "New-Design",
                        "splitPercentage": 50,
                        "priority": 1
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request. `message` explains what to fix.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "examples": {
                  "badName": {
                    "summary": "Invalid template name",
                    "value": {
                      "success": false,
                      "message": "Template name must be 2-100 characters, alphanumeric, hyphens, and underscores only",
                      "data": null
                    }
                  },
                  "noVariants": {
                    "summary": "Empty variants array",
                    "value": {
                      "success": false,
                      "message": "At least one variant is required",
                      "data": null
                    }
                  },
                  "badSplit": {
                    "summary": "Percentages do not sum to 100",
                    "value": {
                      "success": false,
                      "message": "Split percentages must sum to 100 (currently: 90)",
                      "data": null
                    }
                  },
                  "duplicate": {
                    "summary": "Repeated variant name",
                    "value": {
                      "success": false,
                      "message": "Duplicate variant names are not allowed",
                      "data": null
                    }
                  },
                  "lowSplit": {
                    "summary": "Percentage below 1",
                    "value": {
                      "success": false,
                      "message": "Split percentage must be at least 1",
                      "data": null
                    }
                  },
                  "exists": {
                    "summary": "Name already used",
                    "value": {
                      "success": false,
                      "message": "An A/B template named 'homepage-test' already exists",
                      "data": null
                    }
                  },
                  "invalidJson": {
                    "summary": "Malformed body or missing required field",
                    "value": {
                      "success": false,
                      "message": "Invalid JSON format in request. Missing url or required field.",
                      "data": null
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Your plan is not Pro (the message names the Pro feature).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A Pro plan is required for A/B templates"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/ab-templates/{name}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/TemplateName"
        }
      ],
      "get": {
        "operationId": "getAbTemplate",
        "tags": [
          "A/B Templates"
        ],
        "summary": "Get an A/B template",
        "description": "**Requires a Pro plan.** Returns one A/B template by name.",
        "x-gajo-plan": "Pro",
        "responses": {
          "200": {
            "description": "The template.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AbTemplateResult"
                },
                "example": {
                  "success": true,
                  "message": null,
                  "data": {
                    "name": "homepage-test",
                    "description": "New homepage vs. current",
                    "active": true,
                    "createdDate": "2026-10-07T14:03:11.482Z",
                    "updatedDate": null,
                    "variants": [
                      {
                        "variantName": "Control",
                        "splitPercentage": 50,
                        "priority": 0
                      },
                      {
                        "variantName": "New-Design",
                        "splitPercentage": 50,
                        "priority": 1
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Your plan is not Pro (the message names the Pro feature).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A Pro plan is required for A/B templates"
                }
              }
            }
          },
          "404": {
            "description": "No A/B template with this name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A/B template not found",
                  "data": null
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "put": {
        "operationId": "updateAbTemplate",
        "tags": [
          "A/B Templates"
        ],
        "summary": "Replace an A/B template's variants",
        "description": "**Requires a Pro plan.** Replaces the template's description and full list of variants. The body uses the same shape as create; its `name` must be present but is not used to rename (use `PATCH`). Omitting `description` clears it. Links bound to the template pick up the new split immediately; a binding needs an endpoint for every variant, so rebind links after adding or renaming variants.",
        "x-gajo-plan": "Pro",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AbTemplateRequest"
              },
              "example": {
                "name": "homepage-test",
                "description": "Shift traffic to the new design",
                "variants": [
                  {
                    "variantName": "Control",
                    "splitPercentage": 20
                  },
                  {
                    "variantName": "New-Design",
                    "splitPercentage": 80
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated template.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AbTemplateResult"
                },
                "example": {
                  "success": true,
                  "message": "A/B template updated",
                  "data": {
                    "name": "homepage-test",
                    "description": "Shift traffic to the new design",
                    "active": true,
                    "createdDate": "2026-10-07T14:03:11.482Z",
                    "updatedDate": "2026-10-08T09:15:00.250Z",
                    "variants": [
                      {
                        "variantName": "Control",
                        "splitPercentage": 20,
                        "priority": 0
                      },
                      {
                        "variantName": "New-Design",
                        "splitPercentage": 80,
                        "priority": 1
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request. `message` explains what to fix.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "examples": {
                  "noVariants": {
                    "summary": "Empty variants array",
                    "value": {
                      "success": false,
                      "message": "At least one variant is required",
                      "data": null
                    }
                  },
                  "badSplit": {
                    "summary": "Percentages do not sum to 100",
                    "value": {
                      "success": false,
                      "message": "Split percentages must sum to 100 (currently: 90)",
                      "data": null
                    }
                  },
                  "duplicate": {
                    "summary": "Repeated variant name",
                    "value": {
                      "success": false,
                      "message": "Duplicate variant names are not allowed",
                      "data": null
                    }
                  },
                  "lowSplit": {
                    "summary": "Percentage below 1",
                    "value": {
                      "success": false,
                      "message": "Split percentage must be at least 1",
                      "data": null
                    }
                  },
                  "invalidJson": {
                    "summary": "Malformed body or missing required field",
                    "value": {
                      "success": false,
                      "message": "Invalid JSON format in request. Missing url or required field.",
                      "data": null
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Your plan is not Pro (the message names the Pro feature).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A Pro plan is required for A/B templates"
                }
              }
            }
          },
          "404": {
            "description": "No A/B template with this name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A/B template 'homepage-test' not found",
                  "data": null
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "patch": {
        "operationId": "renameAbTemplate",
        "tags": [
          "A/B Templates"
        ],
        "summary": "Rename an A/B template",
        "description": "**Requires a Pro plan.** Renames the template. Existing bindings keep working.",
        "x-gajo-plan": "Pro",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RenameTemplateRequest"
              },
              "example": {
                "newName": "homepage-test-q4"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The renamed template.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AbTemplateResult"
                },
                "example": {
                  "success": true,
                  "message": "A/B template renamed",
                  "data": {
                    "name": "homepage-test-q4",
                    "description": "New homepage vs. current",
                    "active": true,
                    "createdDate": "2026-10-07T14:03:11.482Z",
                    "updatedDate": "2026-10-08T09:15:00.250Z",
                    "variants": [
                      {
                        "variantName": "Control",
                        "splitPercentage": 50,
                        "priority": 0
                      },
                      {
                        "variantName": "New-Design",
                        "splitPercentage": 50,
                        "priority": 1
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request. `message` explains what to fix.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "examples": {
                  "badName": {
                    "summary": "Invalid template name",
                    "value": {
                      "success": false,
                      "message": "Template name must be 2-100 characters, alphanumeric, hyphens, and underscores only",
                      "data": null
                    }
                  },
                  "exists": {
                    "summary": "Name already used",
                    "value": {
                      "success": false,
                      "message": "An A/B template named 'homepage-test-q4' already exists",
                      "data": null
                    }
                  },
                  "invalidJson": {
                    "summary": "Malformed body or missing required field",
                    "value": {
                      "success": false,
                      "message": "Invalid JSON format in request. Missing url or required field.",
                      "data": null
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Your plan is not Pro (the message names the Pro feature).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A Pro plan is required for A/B templates"
                }
              }
            }
          },
          "404": {
            "description": "No A/B template with this name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A/B template 'homepage-test' not found",
                  "data": null
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "delete": {
        "operationId": "deleteAbTemplate",
        "tags": [
          "A/B Templates"
        ],
        "summary": "Delete an A/B template",
        "description": "**Requires a Pro plan.** Deletes the template. Links bound to it stop splitting traffic and fall back to their original destination. The name can be used again right away.",
        "x-gajo-plan": "Pro",
        "responses": {
          "200": {
            "description": "The template was deleted.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResult"
                },
                "example": {
                  "success": true,
                  "message": "A/B template deleted",
                  "data": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Your plan is not Pro (the message names the Pro feature).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A Pro plan is required for A/B templates"
                }
              }
            }
          },
          "404": {
            "description": "No A/B template with this name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A/B template 'homepage-test' not found",
                  "data": null
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/urls/{code}/binding": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ShortCode"
        }
      ],
      "get": {
        "operationId": "getUrlBinding",
        "tags": [
          "Bindings"
        ],
        "summary": "Get a link's binding",
        "description": "**Requires a Pro plan.** Returns the link's template binding and its endpoints, or `{\"shortCode\": ..., \"bound\": false}` when the link is not bound.",
        "x-gajo-plan": "Pro",
        "responses": {
          "200": {
            "description": "The binding.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BindingResult"
                },
                "examples": {
                  "bound": {
                    "summary": "Bound to a route template",
                    "value": {
                      "success": true,
                      "message": null,
                      "data": {
                        "shortCode": "Hn7Kp",
                        "bound": true,
                        "routingType": "Device",
                        "templateName": "app-store-router",
                        "routeTemplateId": 17,
                        "abTemplateId": null,
                        "createdDate": "2026-10-07T14:03:11.482Z",
                        "endpoints": [
                          {
                            "slotKey": "IOS",
                            "targetType": "Url",
                            "targetUrl": "myapp://product/42",
                            "fallbackUrl": "https://apps.apple.com/app/example/id123456789",
                            "routeTemplateName": null,
                            "routeTemplateId": null,
                            "endpointName": null
                          },
                          {
                            "slotKey": "Android",
                            "targetType": "Url",
                            "targetUrl": "myapp://product/42",
                            "fallbackUrl": "https://play.google.com/store/apps/details?id=com.example.app",
                            "routeTemplateName": null,
                            "routeTemplateId": null,
                            "endpointName": null
                          },
                          {
                            "slotKey": "Default",
                            "targetType": "Url",
                            "targetUrl": "https://example.com/product/42",
                            "fallbackUrl": null,
                            "routeTemplateName": null,
                            "routeTemplateId": null,
                            "endpointName": null
                          }
                        ]
                      }
                    }
                  },
                  "unbound": {
                    "summary": "Not bound",
                    "value": {
                      "success": true,
                      "message": null,
                      "data": {
                        "shortCode": "Hn7Kp",
                        "bound": false
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Your plan is not Pro (the message names the Pro feature).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A Pro plan is required for template bindings"
                }
              }
            }
          },
          "404": {
            "description": "No link with this code exists on your account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "URL not found",
                  "data": null
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "post": {
        "operationId": "bindUrl",
        "tags": [
          "Bindings"
        ],
        "summary": "Bind a link to a template",
        "description": "**Requires a Pro plan.** Attaches one route template (`templateType: Device`) or A/B template (`templateType: AbTest`) to the link and supplies a concrete destination for each slot. A link has at most one binding; binding again replaces it. Device bindings need an endpoint for every slot in the route template, unless a `Default` endpoint is included. A/B bindings need an endpoint for every variant. Endpoint `targetUrl`s may be app deep links (custom schemes) with a web `fallbackUrl`, such as the App Store or Google Play listing, used when the app is not installed. Takes effect immediately.",
        "x-gajo-plan": "Pro",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BindRequest"
              },
              "examples": {
                "device": {
                  "summary": "Device routing with deep links and store fallbacks",
                  "value": {
                    "templateType": "Device",
                    "templateName": "app-store-router",
                    "endpoints": [
                      {
                        "slotKey": "IOS",
                        "targetUrl": "myapp://product/42",
                        "fallbackUrl": "https://apps.apple.com/app/example/id123456789"
                      },
                      {
                        "slotKey": "Android",
                        "targetUrl": "myapp://product/42",
                        "fallbackUrl": "https://play.google.com/store/apps/details?id=com.example.app"
                      },
                      {
                        "slotKey": "Default",
                        "targetUrl": "https://example.com/product/42"
                      }
                    ]
                  }
                },
                "abTest": {
                  "summary": "A/B split",
                  "value": {
                    "templateType": "AbTest",
                    "templateName": "homepage-test",
                    "endpoints": [
                      {
                        "slotKey": "Control",
                        "targetUrl": "https://example.com"
                      },
                      {
                        "slotKey": "New-Design",
                        "targetUrl": "https://example.com/v2"
                      }
                    ]
                  }
                },
                "abChained": {
                  "summary": "A/B split where one variant routes by device",
                  "value": {
                    "templateType": "AbTest",
                    "templateName": "homepage-test",
                    "endpoints": [
                      {
                        "slotKey": "Control",
                        "targetType": "RouteTemplate",
                        "routeTemplateName": "app-store-router"
                      },
                      {
                        "slotKey": "Control:IOS",
                        "targetUrl": "https://apps.apple.com/app/example/id123456789"
                      },
                      {
                        "slotKey": "Control:Default",
                        "targetUrl": "https://example.com"
                      },
                      {
                        "slotKey": "New-Design",
                        "targetUrl": "https://example.com/v2"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new binding.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BindingCreatedResult"
                },
                "examples": {
                  "device": {
                    "summary": "Route template with deep links",
                    "value": {
                      "success": true,
                      "message": "URL bound to Device template 'app-store-router'",
                      "data": {
                        "shortCode": "Hn7Kp",
                        "routingType": "Device",
                        "templateName": "app-store-router",
                        "endpoints": [
                          {
                            "slotKey": "IOS",
                            "targetType": "Url",
                            "targetUrl": "myapp://product/42",
                            "fallbackUrl": "https://apps.apple.com/app/example/id123456789",
                            "routeTemplateName": null,
                            "routeTemplateId": null,
                            "endpointName": null
                          },
                          {
                            "slotKey": "Android",
                            "targetType": "Url",
                            "targetUrl": "myapp://product/42",
                            "fallbackUrl": "https://play.google.com/store/apps/details?id=com.example.app",
                            "routeTemplateName": null,
                            "routeTemplateId": null,
                            "endpointName": null
                          },
                          {
                            "slotKey": "Default",
                            "targetType": "Url",
                            "targetUrl": "https://example.com/product/42",
                            "fallbackUrl": null,
                            "routeTemplateName": null,
                            "routeTemplateId": null,
                            "endpointName": null
                          }
                        ]
                      }
                    }
                  },
                  "abTest": {
                    "summary": "A/B template",
                    "value": {
                      "success": true,
                      "message": "URL bound to AbTest template 'homepage-test'",
                      "data": {
                        "shortCode": "Hn7Kp",
                        "routingType": "AbTest",
                        "templateName": "homepage-test",
                        "endpoints": [
                          {
                            "slotKey": "Control",
                            "targetType": "Url",
                            "targetUrl": "https://example.com",
                            "fallbackUrl": null,
                            "routeTemplateName": null,
                            "routeTemplateId": null,
                            "endpointName": null
                          },
                          {
                            "slotKey": "New-Design",
                            "targetType": "Url",
                            "targetUrl": "https://example.com/v2",
                            "fallbackUrl": null,
                            "routeTemplateName": null,
                            "routeTemplateId": null,
                            "endpointName": null
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request. `message` explains what to fix.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "examples": {
                  "badType": {
                    "summary": "Unknown templateType",
                    "value": {
                      "success": false,
                      "message": "Invalid template type: Mobile. Must be Device or AbTest",
                      "data": null
                    }
                  },
                  "templateMissing": {
                    "summary": "Template not found",
                    "value": {
                      "success": false,
                      "message": "Route template 'app-store-router' not found",
                      "data": null
                    }
                  },
                  "badSlot": {
                    "summary": "slotKey not in template",
                    "value": {
                      "success": false,
                      "message": "Endpoint slot 'Phone' does not match any rule in the route template. Expected: IOS, Android, Default",
                      "data": null
                    }
                  },
                  "missingSlots": {
                    "summary": "Slots not covered",
                    "value": {
                      "success": false,
                      "message": "Missing endpoints for: Android. Either provide all slots or include a Default endpoint.",
                      "data": null
                    }
                  },
                  "missingVariants": {
                    "summary": "Variants not covered",
                    "value": {
                      "success": false,
                      "message": "Missing endpoints for variants: New-Design",
                      "data": null
                    }
                  },
                  "fallbackWithoutDeepLink": {
                    "summary": "fallbackUrl on a web destination",
                    "value": {
                      "success": false,
                      "message": "Endpoint 'Default' has a fallback URL but its destination is not an app deep link",
                      "data": null
                    }
                  },
                  "invalidJson": {
                    "summary": "Malformed body or missing required field",
                    "value": {
                      "success": false,
                      "message": "Invalid JSON format in request. Missing url or required field.",
                      "data": null
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Your plan is not Pro (the message names the Pro feature).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A Pro plan is required for template bindings"
                }
              }
            }
          },
          "404": {
            "description": "No link with this code exists on your account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "URL not found",
                  "data": null
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "delete": {
        "operationId": "unbindUrl",
        "tags": [
          "Bindings"
        ],
        "summary": "Remove a link's binding",
        "description": "**Requires a Pro plan.** Removes the link's template binding and endpoints. The link returns to its per-URL routing rules, if any, or its original destination. Succeeds even when the link has no binding.",
        "x-gajo-plan": "Pro",
        "responses": {
          "200": {
            "description": "The binding was removed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResult"
                },
                "example": {
                  "success": true,
                  "message": "URL routing binding removed",
                  "data": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Your plan is not Pro (the message names the Pro feature).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "A Pro plan is required for template bindings"
                }
              }
            }
          },
          "404": {
            "description": "No link with this code exists on your account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResult"
                },
                "example": {
                  "success": false,
                  "message": "URL not found",
                  "data": null
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "Your Gajo API key. Generate one on the API Keys page of your account (https://gajo.io/account/keys)."
      }
    },
    "headers": {
      "X-RateLimit-Limit": {
        "description": "Requests allowed per 60-second sliding window for your plan (10 Free, 30 Basic, 60 Pro).",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "X-RateLimit-Remaining": {
        "description": "Requests left in the current 60-second window.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "Retry-After": {
        "description": "Seconds to wait before retrying.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "X-Total-Count": {
        "description": "Total number of links on your account.",
        "schema": {
          "type": "integer"
        }
      },
      "Link": {
        "description": "Present when more pages remain: `</api/v1/urls?page=2&limit=100>; rel=\"next\"`.",
        "schema": {
          "type": "string"
        }
      }
    },
    "parameters": {
      "ShortCode": {
        "name": "code",
        "in": "path",
        "required": true,
        "description": "Short code of one of your links (the part after `https://gajo.io/` or `https://gajo.io/c/`).",
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9-]{3,50}$"
        },
        "example": "Xk4mPq"
      },
      "TemplateName": {
        "name": "name",
        "in": "path",
        "required": true,
        "description": "Template name.",
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9_-]{2,100}$"
        }
      },
      "RuleId": {
        "name": "ruleId",
        "in": "path",
        "required": true,
        "description": "Routing rule ID (from `GET /urls/{code}/routing`).",
        "schema": {
          "type": "integer"
        },
        "example": 101
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "The `x-api-key` header is missing, or the key is unknown or revoked.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResult"
            },
            "examples": {
              "missingKey": {
                "summary": "No x-api-key header",
                "value": {
                  "success": false,
                  "message": "Missing x-api-key header"
                }
              },
              "invalidKey": {
                "summary": "Unknown or revoked key",
                "value": {
                  "success": false,
                  "message": "Invalid API key"
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Per-minute rate limit exceeded. Wait `Retry-After` seconds.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResult"
            },
            "example": {
              "success": false,
              "message": "Rate limit exceeded. Please try again later."
            }
          }
        }
      },
      "UrlNotFound": {
        "description": "No link with this code on your account (links belonging to other accounts answer the same way).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResult"
            },
            "example": {
              "success": false,
              "message": "URL not found",
              "data": null
            }
          }
        },
        "headers": {
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          }
        }
      },
      "ServerError": {
        "description": "Unexpected server error.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResult"
            },
            "examples": {
              "internal": {
                "summary": "Unexpected error",
                "value": {
                  "success": false,
                  "message": "Internal server error",
                  "data": null
                }
              },
              "internalShort": {
                "summary": "Missing record",
                "value": {
                  "success": false,
                  "message": "Internal error",
                  "data": null
                }
              }
            }
          }
        },
        "headers": {
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          }
        }
      }
    },
    "schemas": {
      "ApiResult": {
        "type": "object",
        "description": "Envelope shared by every response.",
        "required": [
          "success"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "description": "`true` when the request succeeded."
          },
          "message": {
            "type": [
              "string",
              "null"
            ],
            "description": "Human-readable status or error message. Often `null` on reads."
          },
          "data": {
            "description": "Operation-specific payload; `null` or absent when there is nothing to return."
          }
        }
      },
      "ErrorResult": {
        "type": "object",
        "description": "Error envelope. `success` is always `false` and `message` explains the problem.",
        "required": [
          "success",
          "message"
        ],
        "properties": {
          "success": {
            "const": false
          },
          "message": {
            "type": "string"
          },
          "data": {
            "type": "null",
            "description": "Always `null` (or absent) on errors."
          }
        }
      },
      "MessageResult": {
        "description": "Success envelope with a confirmation message and no payload.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiResult"
          },
          {
            "type": "object",
            "required": [
              "message"
            ],
            "properties": {
              "success": {
                "const": true
              },
              "message": {
                "type": "string"
              },
              "data": {
                "type": "null"
              }
            }
          }
        ]
      },
      "Plan": {
        "type": "string",
        "enum": [
          "Free",
          "Basic",
          "Pro"
        ],
        "description": "Gajo account plan."
      },
      "DeviceSlot": {
        "type": "string",
        "enum": [
          "Android",
          "IOS",
          "Mobile",
          "Tablet",
          "Desktop",
          "Default"
        ],
        "description": "Device slot used by route templates and Device bindings. At redirect time OS-specific slots (`Android`, `IOS`) are matched first, then device classes (`Mobile`, `Tablet`, `Desktop`), then `Default` catches everything else."
      },
      "ShortUrl": {
        "type": "object",
        "description": "A short link. Generated codes redirect at `https://gajo.io/{shortCode}`; custom codes (`isCustomCode: true`, created in the Pro dashboard) redirect at `https://gajo.io/c/{shortCode}`. All redirects are HTTP 302.",
        "required": [
          "shortCode",
          "originalUrl",
          "fallbackUrl",
          "active",
          "clickCount",
          "isCustomCode",
          "createdDate",
          "expiresDate"
        ],
        "properties": {
          "shortCode": {
            "type": "string",
            "description": "The short code. Generated codes are never reassigned."
          },
          "originalUrl": {
            "type": "string",
            "description": "Destination URL (may be an app deep link on Pro)."
          },
          "fallbackUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Web fallback used when `originalUrl` is an app deep link and the app is not installed (e.g. the App Store listing). `null` otherwise."
          },
          "active": {
            "type": "boolean",
            "description": "`false` once the link has been deactivated."
          },
          "clickCount": {
            "type": "integer",
            "format": "int64",
            "minimum": 0,
            "description": "Human clicks (bot and link-preview traffic excluded)."
          },
          "isCustomCode": {
            "type": "boolean",
            "description": "`true` for Pro custom codes (served under `/c/`)."
          },
          "createdDate": {
            "type": "string",
            "format": "date-time"
          },
          "expiresDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the link stops redirecting; `null` = never expires."
          }
        }
      },
      "ShortenRequest": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "maxLength": 2048,
            "description": "Destination URL. Supported schemes: `http`, `https`, `mailto`, `tel`, `ftp`, `sftp`. Pro plans may also shorten app deep links with a custom scheme (e.g. `myapp://product/42`). URLs pointing at gajo.io are rejected."
          },
          "customCode": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[A-Za-z0-9-]{3,50}$",
            "description": "Pro only. A custom code for the link (3-50 letters, digits, or hyphens; stored lowercase). The link redirects at `https://gajo.io/c/{code}` and the response has `isCustomCode: true`. An unclaimed code is claimed for you; one of your own codes is reused if it isn't linked to a URL. Can't be combined with `lifetimeDays`."
          },
          "lifetimeDays": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "description": "Days until the link expires. Omit for your plan maximum. Values above the plan maximum are clamped to it (60 on Free, 120 on Basic); Pro has no maximum, and omitting it on Pro creates a link that never expires."
          },
          "fallbackUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Only valid when `url` is an app deep link (custom scheme, which requires Pro). A web URL that visitors are sent to when the app is not installed, such as the App Store or Google Play listing. Sending it with an ordinary web `url` is rejected with `400`."
          }
        }
      },
      "BatchShortenRequest": {
        "type": "object",
        "required": [
          "urls"
        ],
        "properties": {
          "urls": {
            "type": "array",
            "maxItems": 100,
            "items": {
              "$ref": "#/components/schemas/ShortenRequest"
            },
            "description": "Up to 100 links, each with the same fields as `POST /shorten`."
          }
        }
      },
      "BatchShortenItemSuccess": {
        "type": "object",
        "required": [
          "success",
          "url"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "url": {
            "$ref": "#/components/schemas/ShortUrl"
          }
        }
      },
      "BatchShortenItemFailure": {
        "type": "object",
        "required": [
          "success",
          "error",
          "originalUrl"
        ],
        "properties": {
          "success": {
            "const": false
          },
          "error": {
            "type": "string",
            "description": "Why this item was not shortened (validation error or monthly limit)."
          },
          "originalUrl": {
            "type": "string",
            "description": "The `url` from the request item."
          }
        }
      },
      "BatchShortenItem": {
        "description": "Per-item outcome, in request order.",
        "oneOf": [
          {
            "$ref": "#/components/schemas/BatchShortenItemSuccess"
          },
          {
            "$ref": "#/components/schemas/BatchShortenItemFailure"
          }
        ]
      },
      "UpdateUrlRequest": {
        "type": "object",
        "description": "Send only the fields to change.",
        "properties": {
          "originalUrl": {
            "type": "string",
            "maxLength": 2048,
            "description": "New destination, validated like `POST /shorten`. App deep links (custom schemes) are allowed."
          },
          "interstitialEnabled": {
            "type": "boolean",
            "description": "Show your branded interstitial page before redirecting."
          }
        }
      },
      "UrlStats": {
        "type": "object",
        "description": "Click analytics for one link. Bot traffic (crawlers, link-preview unfurlers, AI fetchers, HTTP libraries) is excluded. Free plans receive `totalClicks`, `historyDays` (7) and `clicksByDay`. Basic and Pro receive 30 days of history plus `topReferrers`, `deviceBreakdown`, `topCountries` and `topRegions`, which are omitted entirely on Free.",
        "required": [
          "totalClicks",
          "historyDays",
          "clicksByDay"
        ],
        "properties": {
          "totalClicks": {
            "type": "integer",
            "format": "int64",
            "minimum": 0,
            "description": "All-time human clicks."
          },
          "historyDays": {
            "type": "integer",
            "enum": [
              7,
              30
            ],
            "description": "Days covered by `clicksByDay`: 7 on Free, 30 on Basic and Pro."
          },
          "clicksByDay": {
            "type": "array",
            "description": "Daily click counts, oldest first. Days with no clicks are omitted.",
            "items": {
              "type": "object",
              "required": [
                "date",
                "clicks"
              ],
              "properties": {
                "date": {
                  "type": "string",
                  "format": "date"
                },
                "clicks": {
                  "type": "integer",
                  "format": "int64",
                  "minimum": 0
                }
              }
            }
          },
          "topReferrers": {
            "type": "array",
            "description": "Basic and Pro only. Top 10 referrers; `Direct` when no referrer was sent.",
            "items": {
              "type": "object",
              "required": [
                "source",
                "count"
              ],
              "properties": {
                "source": {
                  "type": "string"
                },
                "count": {
                  "type": "integer",
                  "format": "int64"
                }
              }
            }
          },
          "deviceBreakdown": {
            "type": "array",
            "description": "Basic and Pro only. Share of clicks per device class, as percentages (0-100) summing to 100.",
            "items": {
              "type": "object",
              "required": [
                "device",
                "percentage"
              ],
              "properties": {
                "device": {
                  "type": "string",
                  "examples": [
                    "Mobile",
                    "Desktop",
                    "Tablet"
                  ]
                },
                "percentage": {
                  "type": "number",
                  "format": "double",
                  "minimum": 0,
                  "maximum": 100
                }
              }
            }
          },
          "topCountries": {
            "type": "array",
            "description": "Basic and Pro only. Top 10 countries by clicks (country name, or ISO code when the name is unknown).",
            "items": {
              "type": "object",
              "required": [
                "country",
                "count"
              ],
              "properties": {
                "country": {
                  "type": "string"
                },
                "count": {
                  "type": "integer",
                  "format": "int64"
                }
              }
            }
          },
          "topRegions": {
            "type": "array",
            "description": "Basic and Pro only. Top 10 states/provinces, labelled `Region, CC`. May be empty.",
            "items": {
              "type": "object",
              "required": [
                "region",
                "count"
              ],
              "properties": {
                "region": {
                  "type": "string"
                },
                "count": {
                  "type": "integer",
                  "format": "int64"
                }
              }
            }
          }
        }
      },
      "Me": {
        "type": "object",
        "required": [
          "plan",
          "email",
          "urlCount",
          "apiAccess",
          "customCodes",
          "routingRules",
          "memberSince"
        ],
        "properties": {
          "plan": {
            "$ref": "#/components/schemas/Plan"
          },
          "email": {
            "type": "string",
            "description": "Account email address (empty string if it cannot be resolved)."
          },
          "urlCount": {
            "type": "integer",
            "format": "int64",
            "minimum": 0,
            "description": "Total links ever created on the account."
          },
          "apiAccess": {
            "type": "boolean",
            "description": "Whether the plan includes the API (always `true` for account plans)."
          },
          "customCodes": {
            "type": "boolean",
            "description": "Whether the plan can create custom short codes (Pro)."
          },
          "routingRules": {
            "type": "boolean",
            "description": "Whether the plan can use routing, templates, bindings and deep links (Pro)."
          },
          "memberSince": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Usage": {
        "type": "object",
        "required": [
          "plan",
          "urlsCreatedThisMonth",
          "urlsMonthlyLimit",
          "urlsRemaining",
          "requestsPerMinute",
          "maxLinkLifetimeDays",
          "totalUrls",
          "totalClicks"
        ],
        "properties": {
          "plan": {
            "$ref": "#/components/schemas/Plan"
          },
          "urlsCreatedThisMonth": {
            "type": "integer",
            "minimum": 0,
            "description": "Links created since the start of the calendar month (UTC), via the API or dashboard."
          },
          "urlsMonthlyLimit": {
            "type": "integer",
            "minimum": 1,
            "description": "Monthly link allowance for the plan (50 Free, 500 Basic, 10,000 Pro)."
          },
          "urlsRemaining": {
            "type": "integer",
            "minimum": 0,
            "description": "Links you can still create this month."
          },
          "requestsPerMinute": {
            "type": "integer",
            "minimum": 1,
            "description": "API requests allowed per 60-second window (10 Free, 30 Basic, 60 Pro)."
          },
          "maxLinkLifetimeDays": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "description": "Longest allowed link lifetime in days; `null` = unlimited (Pro)."
          },
          "totalUrls": {
            "type": "integer",
            "minimum": 0,
            "description": "All links on the account."
          },
          "totalClicks": {
            "type": "integer",
            "format": "int64",
            "minimum": 0,
            "description": "Human clicks across all links."
          }
        }
      },
      "RoutingType": {
        "type": "string",
        "enum": [
          "Device",
          "AbTest"
        ],
        "description": "`Device` routes by device class; `AbTest` splits traffic by percentage."
      },
      "DeviceClass": {
        "type": "string",
        "enum": [
          "Mobile",
          "Tablet",
          "Desktop"
        ],
        "description": "Device class for per-URL `Device` rules."
      },
      "RoutingRule": {
        "type": "object",
        "required": [
          "id",
          "ruleType",
          "priority",
          "active",
          "deviceType",
          "targetUrl",
          "variantName",
          "splitPercentage",
          "createdDate"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Rule ID, used by `PUT`/`DELETE /urls/{code}/routing/{ruleId}`."
          },
          "ruleType": {
            "$ref": "#/components/schemas/RoutingType"
          },
          "priority": {
            "type": "integer",
            "minimum": 0,
            "description": "Position within the batch the rule was created in (0-based)."
          },
          "active": {
            "type": "boolean"
          },
          "deviceType": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/DeviceClass"
              },
              {
                "type": "null"
              }
            ],
            "description": "Set on `Device` rules; `null` on `AbTest` rules."
          },
          "targetUrl": {
            "type": "string",
            "description": "Destination for matching clicks."
          },
          "variantName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Set on `AbTest` rules."
          },
          "splitPercentage": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 100,
            "description": "Set on `AbTest` rules."
          },
          "createdDate": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RoutingRuleInput": {
        "type": "object",
        "required": [
          "ruleType",
          "targetUrl"
        ],
        "properties": {
          "ruleType": {
            "$ref": "#/components/schemas/RoutingType"
          },
          "deviceType": {
            "$ref": "#/components/schemas/DeviceClass",
            "description": "Required for `Device` rules. Each device type may appear once."
          },
          "targetUrl": {
            "type": "string",
            "maxLength": 2048,
            "description": "Destination URL (standard web schemes; validated like `POST /shorten`)."
          },
          "variantName": {
            "type": "string",
            "description": "Required for `AbTest` rules."
          },
          "splitPercentage": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Required for `AbTest` rules; all percentages in the request must sum to 100."
          }
        }
      },
      "CreateRoutingRulesRequest": {
        "type": "object",
        "required": [
          "rules"
        ],
        "properties": {
          "rules": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/RoutingRuleInput"
            },
            "description": "One or more rules, all with the same `ruleType`."
          }
        }
      },
      "UpdateRoutingRuleRequest": {
        "type": "object",
        "description": "Send only the fields to change.",
        "properties": {
          "targetUrl": {
            "type": "string",
            "maxLength": 2048
          },
          "splitPercentage": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "active": {
            "type": "boolean"
          }
        }
      },
      "RoutingOverview": {
        "type": "object",
        "required": [
          "shortCode",
          "rules",
          "binding"
        ],
        "properties": {
          "shortCode": {
            "type": "string"
          },
          "rules": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RoutingRule"
            },
            "description": "Per-URL rules, ordered by priority."
          },
          "binding": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Binding"
              },
              {
                "type": "null"
              }
            ],
            "description": "The link's active template binding, or `null` when it has none. A binding takes precedence over per-URL rules."
          }
        }
      },
      "RoutingRulesCreated": {
        "type": "object",
        "required": [
          "shortCode",
          "ruleType",
          "rules"
        ],
        "properties": {
          "shortCode": {
            "type": "string"
          },
          "ruleType": {
            "$ref": "#/components/schemas/RoutingType"
          },
          "rules": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RoutingRule"
            },
            "description": "The rules created by this request."
          }
        }
      },
      "RouteTemplate": {
        "type": "object",
        "description": "An abstract device-routing template: device slots only, no URLs.",
        "required": [
          "name",
          "description",
          "active",
          "createdDate",
          "updatedDate",
          "rules"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "active": {
            "type": "boolean"
          },
          "createdDate": {
            "type": "string",
            "format": "date-time"
          },
          "updatedDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "rules": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "deviceType",
                "priority"
              ],
              "properties": {
                "deviceType": {
                  "$ref": "#/components/schemas/DeviceSlot"
                },
                "priority": {
                  "type": "integer",
                  "minimum": 0,
                  "description": "Position in the template (0-based, request order)."
                }
              }
            }
          }
        }
      },
      "RouteTemplateRequest": {
        "type": "object",
        "required": [
          "name",
          "rules"
        ],
        "properties": {
          "name": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]{2,100}$",
            "description": "2-100 characters: letters, digits, hyphens and underscores. Unique per account."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional note. Omitting it on update clears it."
          },
          "rules": {
            "type": "array",
            "minItems": 1,
            "maxItems": 6,
            "description": "Device slots in this template; each `deviceType` may appear once. Include `Default` to catch unmatched devices.",
            "items": {
              "type": "object",
              "required": [
                "deviceType"
              ],
              "properties": {
                "deviceType": {
                  "$ref": "#/components/schemas/DeviceSlot"
                }
              }
            }
          }
        }
      },
      "AbTemplate": {
        "type": "object",
        "description": "An abstract A/B split template: variant names and percentages only, no URLs.",
        "required": [
          "name",
          "description",
          "active",
          "createdDate",
          "updatedDate",
          "variants"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "active": {
            "type": "boolean"
          },
          "createdDate": {
            "type": "string",
            "format": "date-time"
          },
          "updatedDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "variants": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "variantName",
                "splitPercentage",
                "priority"
              ],
              "properties": {
                "variantName": {
                  "type": "string"
                },
                "splitPercentage": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 100
                },
                "priority": {
                  "type": "integer",
                  "minimum": 0,
                  "description": "Position in the template (0-based, request order)."
                }
              }
            }
          }
        }
      },
      "AbTemplateRequest": {
        "type": "object",
        "required": [
          "name",
          "variants"
        ],
        "properties": {
          "name": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]{2,100}$",
            "description": "2-100 characters: letters, digits, hyphens and underscores. Unique per account."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional note. Omitting it on update clears it."
          },
          "variants": {
            "type": "array",
            "minItems": 1,
            "description": "Variants with unique names; `splitPercentage` values must sum to 100.",
            "items": {
              "type": "object",
              "required": [
                "variantName",
                "splitPercentage"
              ],
              "properties": {
                "variantName": {
                  "type": "string",
                  "minLength": 1
                },
                "splitPercentage": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 100
                }
              }
            }
          }
        }
      },
      "RenameTemplateRequest": {
        "type": "object",
        "required": [
          "newName"
        ],
        "properties": {
          "newName": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]{2,100}$",
            "description": "2-100 characters: letters, digits, hyphens and underscores. Unique per account."
          }
        }
      },
      "EndpointTarget": {
        "type": "string",
        "enum": [
          "Url",
          "RouteTemplate"
        ],
        "description": "`Url` (default) sends the slot to `targetUrl`. `RouteTemplate` is reserved for chaining an A/B variant to a route template; this is not fully supported yet, so use `Url` targets."
      },
      "Endpoint": {
        "type": "object",
        "description": "A concrete destination for one slot of a binding.",
        "required": [
          "slotKey",
          "targetType",
          "targetUrl",
          "fallbackUrl",
          "routeTemplateName",
          "routeTemplateId",
          "endpointName"
        ],
        "properties": {
          "slotKey": {
            "type": "string",
            "description": "Device slot (Device bindings) or variant name (A/B bindings)."
          },
          "targetType": {
            "$ref": "#/components/schemas/EndpointTarget"
          },
          "targetUrl": {
            "type": [
              "string",
              "null"
            ]
          },
          "fallbackUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Web fallback when `targetUrl` is an app deep link and the app is not installed."
          },
          "routeTemplateName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the chained route template when `targetType` is `RouteTemplate` (`null` otherwise, or if it was deleted)."
          },
          "routeTemplateId": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Numeric ID of the chained route template when `targetType` is `RouteTemplate`. Deprecated: use `routeTemplateName`.",
            "deprecated": true
          },
          "endpointName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional label."
          }
        }
      },
      "BindingEndpoint": {
        "type": "object",
        "required": [
          "slotKey"
        ],
        "properties": {
          "slotKey": {
            "description": "For `Device` bindings, one of the template's device slots. For `AbTest` bindings, one of the template's variant names, or `Variant:Device` (e.g. `Control:IOS`) for the device destinations of a variant that chains to a route template.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/DeviceSlot"
              },
              {
                "type": "string",
                "description": "A/B variant name"
              }
            ]
          },
          "targetType": {
            "$ref": "#/components/schemas/EndpointTarget",
            "default": "Url"
          },
          "targetUrl": {
            "type": "string",
            "maxLength": 2048,
            "description": "Required when `targetType` is `Url`. May be an app deep link (custom scheme)."
          },
          "fallbackUrl": {
            "type": "string",
            "description": "Only when `targetUrl` is an app deep link: web URL (e.g. App Store / Google Play listing) used when the app is not installed."
          },
          "routeTemplateName": {
            "type": "string",
            "description": "With `targetType: RouteTemplate` (A/B bindings): the route template this variant chains to. Add the variant's per-device destinations as endpoints keyed `Variant:Device`, covering every slot of the route template or including `Variant:Default`."
          },
          "endpointName": {
            "type": "string",
            "description": "Optional label for this endpoint."
          }
        }
      },
      "BindRequest": {
        "type": "object",
        "required": [
          "templateType",
          "templateName",
          "endpoints"
        ],
        "properties": {
          "templateType": {
            "$ref": "#/components/schemas/RoutingType",
            "description": "`Device` to bind a route template, `AbTest` to bind an A/B template."
          },
          "templateName": {
            "type": "string",
            "description": "Name of your route template or A/B template."
          },
          "endpoints": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BindingEndpoint"
            },
            "description": "One endpoint per slot. Device bindings must cover every template slot unless a `Default` endpoint is included; A/B bindings must cover every variant, plus `Variant:Device` endpoints for any variant that chains to a route template."
          },
          "flowJson": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional layout state for the dashboard's visual Route Builder. API clients can omit it."
          }
        }
      },
      "Binding": {
        "type": "object",
        "description": "A link's template binding. When `bound` is `false`, only `shortCode` and `bound` are returned.",
        "required": [
          "bound"
        ],
        "properties": {
          "shortCode": {
            "type": "string"
          },
          "bound": {
            "type": "boolean"
          },
          "routingType": {
            "$ref": "#/components/schemas/RoutingType",
            "description": "`Device` (route template) or `AbTest` (A/B template)."
          },
          "templateName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the bound route or A/B template, or `null` if it was deleted (the link then uses its original destination)."
          },
          "routeTemplateId": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Numeric ID of the bound route template (`null` for A/B bindings). Deprecated: use `templateName`.",
            "deprecated": true
          },
          "abTemplateId": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Numeric ID of the bound A/B template (`null` for Device bindings). Deprecated: use `templateName`.",
            "deprecated": true
          },
          "createdDate": {
            "type": "string",
            "format": "date-time"
          },
          "endpoints": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Endpoint"
            }
          }
        }
      },
      "BindingCreated": {
        "type": "object",
        "required": [
          "shortCode",
          "routingType",
          "templateName",
          "endpoints"
        ],
        "properties": {
          "shortCode": {
            "type": "string"
          },
          "routingType": {
            "$ref": "#/components/schemas/RoutingType"
          },
          "templateName": {
            "type": "string"
          },
          "endpoints": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Endpoint"
            }
          }
        }
      },
      "ShortUrlResult": {
        "description": "Envelope with one short link.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiResult"
          },
          {
            "type": "object",
            "required": [
              "data"
            ],
            "properties": {
              "success": {
                "const": true
              },
              "data": {
                "$ref": "#/components/schemas/ShortUrl"
              }
            }
          }
        ]
      },
      "ShortUrlListResult": {
        "description": "Envelope with a list of short links.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiResult"
          },
          {
            "type": "object",
            "required": [
              "data"
            ],
            "properties": {
              "success": {
                "const": true
              },
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ShortUrl"
                }
              }
            }
          }
        ]
      },
      "BatchShortenResult": {
        "description": "Envelope with per-item batch results.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiResult"
          },
          {
            "type": "object",
            "required": [
              "data"
            ],
            "properties": {
              "success": {
                "const": true
              },
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/BatchShortenItem"
                }
              }
            }
          }
        ]
      },
      "UrlStatsResult": {
        "description": "Envelope with click analytics.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiResult"
          },
          {
            "type": "object",
            "required": [
              "data"
            ],
            "properties": {
              "success": {
                "const": true
              },
              "data": {
                "$ref": "#/components/schemas/UrlStats"
              }
            }
          }
        ]
      },
      "MeResult": {
        "description": "Envelope with account details.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiResult"
          },
          {
            "type": "object",
            "required": [
              "data"
            ],
            "properties": {
              "success": {
                "const": true
              },
              "data": {
                "$ref": "#/components/schemas/Me"
              }
            }
          }
        ]
      },
      "UsageResult": {
        "description": "Envelope with usage details.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiResult"
          },
          {
            "type": "object",
            "required": [
              "data"
            ],
            "properties": {
              "success": {
                "const": true
              },
              "data": {
                "$ref": "#/components/schemas/Usage"
              }
            }
          }
        ]
      },
      "RoutingOverviewResult": {
        "description": "Envelope with a link's routing rules and binding.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiResult"
          },
          {
            "type": "object",
            "required": [
              "data"
            ],
            "properties": {
              "success": {
                "const": true
              },
              "data": {
                "$ref": "#/components/schemas/RoutingOverview"
              }
            }
          }
        ]
      },
      "RoutingRulesCreatedResult": {
        "description": "Envelope with newly created routing rules.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiResult"
          },
          {
            "type": "object",
            "required": [
              "data"
            ],
            "properties": {
              "success": {
                "const": true
              },
              "data": {
                "$ref": "#/components/schemas/RoutingRulesCreated"
              }
            }
          }
        ]
      },
      "RoutingRuleResult": {
        "description": "Envelope with one routing rule.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiResult"
          },
          {
            "type": "object",
            "required": [
              "data"
            ],
            "properties": {
              "success": {
                "const": true
              },
              "data": {
                "$ref": "#/components/schemas/RoutingRule"
              }
            }
          }
        ]
      },
      "RouteTemplateResult": {
        "description": "Envelope with one route template.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiResult"
          },
          {
            "type": "object",
            "required": [
              "data"
            ],
            "properties": {
              "success": {
                "const": true
              },
              "data": {
                "$ref": "#/components/schemas/RouteTemplate"
              }
            }
          }
        ]
      },
      "RouteTemplateListResult": {
        "description": "Envelope with route templates.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiResult"
          },
          {
            "type": "object",
            "required": [
              "data"
            ],
            "properties": {
              "success": {
                "const": true
              },
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/RouteTemplate"
                }
              }
            }
          }
        ]
      },
      "AbTemplateResult": {
        "description": "Envelope with one A/B template.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiResult"
          },
          {
            "type": "object",
            "required": [
              "data"
            ],
            "properties": {
              "success": {
                "const": true
              },
              "data": {
                "$ref": "#/components/schemas/AbTemplate"
              }
            }
          }
        ]
      },
      "AbTemplateListResult": {
        "description": "Envelope with A/B templates.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiResult"
          },
          {
            "type": "object",
            "required": [
              "data"
            ],
            "properties": {
              "success": {
                "const": true
              },
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/AbTemplate"
                }
              }
            }
          }
        ]
      },
      "BindingResult": {
        "description": "Envelope with a link's binding.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiResult"
          },
          {
            "type": "object",
            "required": [
              "data"
            ],
            "properties": {
              "success": {
                "const": true
              },
              "data": {
                "$ref": "#/components/schemas/Binding"
              }
            }
          }
        ]
      },
      "BindingCreatedResult": {
        "description": "Envelope with a newly created binding.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiResult"
          },
          {
            "type": "object",
            "required": [
              "data"
            ],
            "properties": {
              "success": {
                "const": true
              },
              "data": {
                "$ref": "#/components/schemas/BindingCreated"
              }
            }
          }
        ]
      }
    }
  }
}
