Endpoints
All endpoints share the base path:
https://api.codelloy.com/link/external/v1Authentication is by X-API-KEY on every request. See Obtaining Your API Key to generate one and Mobile Apps for the deep-link setup that the schema below references via mobileAppId.
Every response is wrapped in the standard envelope { response: …, errors: … }. On success, response is the payload and errors is omitted; on failure, response is omitted and errors is a non-empty array. See API Errors for the full error catalogue.
Create or Update a Short URL
Creates a new short URL or updates an existing one (matched by shortCode).
POST /link/external/v1/shortenedUrl/saveRequest Headers
| Header | Value | Required |
|---|---|---|
X-API-KEY | Your API key | Yes |
Content-Type | application/json | Yes |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Destination URL. Must be non-empty. |
shortCode | string | No | Custom alias for the short link. 7-50 characters, alphanumeric / hyphen / underscore only (regex ^[a-zA-Z0-9_-]{7,50}$). Auto-generated when omitted. Re-using an existing shortCode belonging to your organisation updates that record. |
shortLinkName | string | No | Human-readable label shown in the dashboard list view. Strongly recommended for API-created links so operators can find them. |
customDomain | string | No | One of your organisation’s active custom domains to serve this link on (e.g. go.yourbrand.com). Omit to use your organisation’s primary domain. A value that isn’t an active domain of your org is rejected with 400 (VE001). The response smartLink reflects the resulting host. Set up and verify the domain on the dashboard first — see Custom Domains. |
isActive | boolean | No | Defaults to true. Set to false to archive the link without deleting it (clients get HTTP 410 EN002 at click time). |
cloakEnabled | boolean | No | false on create. On an update, omitting it leaves the stored value alone — it does not reset to false, so send false explicitly to un-cloak a link. When true, the destination is rendered inside a full-viewport frame instead of being navigated to, so your short-link host stays in the visitor’s address bar for the whole visit. Accepted on any link. See the cloaking note below for what it does not do. |
utmSource | string | No | UTM source tag (appended to the destination URL at click time). |
utmCampaign | string | No | UTM campaign tag. |
utmMedium | string | No | UTM medium tag. |
enableCustomMetadata | boolean | No | When true, the customMetadata* fields below override the destination URL’s Open Graph preview for social-sharing surfaces. Requires the Metadata Preview feature on your plan. |
customMetadataTitle | string | No | Override title for link previews. |
customMetadataDescription | string | No | Override description for link previews. |
customMetadataImageUrl | string | No | Override image URL for link previews. |
deepLinks | array | No | Array of DeepLink objects (see sub-table below). Omit or pass [] for a plain redirect-only short URL. |
DeepLink Entry Schema
Each entry in the deepLinks[] array follows this shape.
| Field | Type | Direction | Description |
|---|---|---|---|
mobileAppId | string | Required on request | The 12-character ID from your Mobile apps page. Identifies the registered MobileApp this entry targets. Null/blank/unknown values return DL001. |
deeplinkUrl | string | Request | Custom-scheme URI used when the app is installed (e.g. myapp://product/123). |
fallbackUrl | string | Request | Web URL used when fallbackEnabled=true and the app is not installed. |
linkBehaviour | string | Request | "app" (try the deep link first) or "webBrowser" (skip the app and go straight to url). |
fallbackEnabled | boolean | Request | When true, prefer fallbackUrl over the registered Store URL on app-not-installed. When false, prefer the Store URL. |
platform | string | Response only (server-controlled) | Reflects the registered MobileApp.platform ("IOS" / "ANDROID" / "WEB"). Values supplied in the request body are silently overwritten on save. |
storeUrl | string | Response only (server-controlled) | Reflects the registered MobileApp.storeUrl at read time. Edits on the Mobile apps page are picked up immediately on the next read. |
Example Request
curl -X POST https://api.codelloy.com/link/external/v1/shortenedUrl/save \
-H "X-API-KEY: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/product/123",
"shortCode": "spring-sale",
"shortLinkName": "Spring sale — product 123",
"customDomain": "go.yourbrand.com",
"deepLinks": [
{
"mobileAppId": "abc123def456",
"deeplinkUrl": "myapp://product/123",
"fallbackUrl": "https://example.com/product/123",
"linkBehaviour": "app",
"fallbackEnabled": true
},
{
"mobileAppId": "789xyz456ghi",
"deeplinkUrl": "myapp://product/123",
"fallbackUrl": "https://example.com/product/123",
"linkBehaviour": "app",
"fallbackEnabled": true
}
]
}'Success Response
HTTP/1.1 200 OK
Content-Type: application/json{
"response": {
"url": "https://example.com/product/123",
"shortCode": "spring-sale",
"shortLinkName": "Spring sale — product 123",
"smartLink": "https://go.yourbrand.com/spring-sale",
"isActive": true,
"cloakEnabled": false,
"deepLinks": [
{
"mobileAppId": "abc123def456",
"platform": "IOS",
"storeUrl": "https://apps.apple.com/app/myapp/id123456789",
"deeplinkUrl": "myapp://product/123",
"fallbackUrl": "https://example.com/product/123",
"linkBehaviour": "app",
"fallbackEnabled": true,
"appId": 42
},
{
"mobileAppId": "789xyz456ghi",
"platform": "ANDROID",
"storeUrl": "https://play.google.com/store/apps/details?id=com.example.myapp",
"deeplinkUrl": "myapp://product/123",
"fallbackUrl": "https://example.com/product/123",
"linkBehaviour": "app",
"fallbackEnabled": true,
"appId": 43
}
],
"createdAt": "2026-05-15T10:30:00Z",
"updatedAt": "2026-05-15T10:30:00Z",
"createdBy": "api-key:Production Integration",
"updatedBy": "api-key:Production Integration",
"createdVia": "EXTERNAL",
"lastModifiedVia": "EXTERNAL"
}
}Custom domains:
smartLinkreflects your organisation’s active custom domain when the link is created (e.g.https://go.yourbrand.com/spring-sale). If the custom domain is later removed,https://re.codelloy.com/{shortCode}continues to resolve as a fallback.
Audit attribution:
createdByandupdatedBystart withapi-key:for any write performed through the External API, followed by the (required) label of the key that made the request. See Obtaining Your API Key for the labelling flow.
Link cloaking
cloakEnabled keeps your short-link host in the visitor’s address bar by rendering the destination in a full-viewport frame instead of navigating to it. Several things to know before you switch it on.
It is branding, not concealment. The destination URL stays visible in the page source, in the browser’s network tab, and in link previews. Do not present a cloaked link to your own users as private or hidden.
The API does not check whether your destination can be framed. There is no error to alert you.
Many sites refuse to be framed by anyone. They send X-Frame-Options: DENY or SAMEORIGIN, or a CSP frame-ancestors directive that excludes your domain. YouTube, most banks and many SaaS login pages all do.
We never check this when you save a link. A cloaked link to a destination that refuses framing is created exactly as you asked, and returns 200:
POST /link/external/v1/shortenedUrl/save → HTTP 200 (response abridged):
{
"response": {
"shortCode": "fca66b8",
"url": "https://www.youtube.com/watch?v=...",
"cloakEnabled": true
}
}cloakEnabled stays true. It is not silently downgraded to false, and no warning is returned — not in the body, not in a header.
What your visitors get is a blank page. Not an error page, not a redirect, not a 4xx. The short link returns HTTP 200, the frame is created at full size pointing at the right destination, and nothing renders inside it. If you are ever handed a bug report that says “the link is broken, but it loads fine”, this is almost certainly the cause.
Opening the link in a browser is the only check that exists. Do it before you roll cloaking out:
# 1. Read the destination's framing headers. Use GET, not HEAD (-I): a meaningful
# minority of servers answer HEAD differently from the request a browser makes.
curl -sS -D - -o /dev/null "https://example.com/landing" \
| grep -i "^x-frame-options:\|frame-ancestors"
# 2. Open the returned smartLink in a browser and confirm you see the page, not a blank paneRead the output as a browser would, not as a keyword match:
X-Frame-Options: DENYorSAMEORIGIN— it will not frame.- A CSP
frame-ancestorsdirective that does not name your short-link host — it will not frame. Note the host that matters is the one the link is served on: your custom domain if the link is branded,re.codelloy.comif it is not. - A CSP with no
frame-ancestorsdirective does not block framing at all. Nearly every modern site sends someContent-Security-Policy;default-src,script-srcand the rest say nothing about who may embed the page. Grepping for the header name alone will tell you almost every destination refuses, which is wrong. - No relevant header — it will frame, today. See “Nothing re-checks after the fact” below.
Nothing re-checks after the fact. A destination that frames happily today can add X-Frame-Options at any time, and every cloaked link pointing at it goes blank from that moment on. No notification is sent, because the change happens on their servers, not ours. If you cloak links to destinations you do not control, re-test them periodically.
The Codelloy dashboard behaves differently, deliberately. Links created in the dashboard are probed first, and the save is blocked if the destination definitely refuses framing. The API has no such guardrail on purpose: probing would make every write depend on a third party’s uptime and add seconds of latency to your integration. So a link the dashboard refuses to create is one the API will create without comment.
It is returned on every read. "cloakEnabled": false comes back on every link you read, including links created before cloaking existed and links you never set the field on — a link created without the field is stored as false. See the Field Reference for the canonical row.
You cannot un-cloak a link by omitting the field. save is an upsert, and on the update half an omitted field leaves the stored value untouched — that is what makes it safe to send a partial body. So a link stored with cloakEnabled: true stays cloaked no matter how many times you re-POST it without the field, and no error tells you the request did nothing. Send "cloakEnabled": false explicitly.
Validation Errors
A missing required field returns HTTP 400. Bean Validation errors emit SML002; ad-hoc validation (e.g. invalid shortCode format) emits VE001. Branch on the code field, not the HTTP status.
A rejected shortCode also carries details.field (always shortCode) and a details.reason of FORMAT, RESERVED or TAKEN — the status, code and message are unchanged, so existing branching keeps working.
HTTP/1.1 400 Bad Request
Content-Type: application/json{
"errors": [
{
"code": "SML002",
"message": "must not be empty"
}
]
}A deepLinks[] entry with a missing or unknown mobileAppId returns HTTP 400 with DL001:
{
"errors": [
{
"code": "DL001",
"message": "Deep link entry rejected: mobileAppId 'unknown123' is not recognised for this organisation",
"details": {
"kind": "deep_link_validation",
"mobileAppId": "unknown123"
}
}
]
}Bulk Create or Update Short URLs
Creates or updates up to 30 short URLs in a single request.
POST /link/external/v1/shortenedUrl/bulkSaveRequest Headers
| Header | Value | Required |
|---|---|---|
X-API-KEY | Your API key | Yes |
Content-Type | application/json | Yes |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
data | array | Yes | Array of short-URL objects (same fields as the single save endpoint). Max 30 items. |
Example Request
curl -X POST https://api.codelloy.com/link/external/v1/shortenedUrl/bulkSave \
-H "X-API-KEY: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"data": [
{ "url": "https://example.com/page-one", "shortCode": "page-one", "shortLinkName": "Page One" },
{ "url": "https://example.com/page-two", "shortLinkName": "Page Two" }
]
}'Success Response
A single envelope wraps two arrays: successRecords (saved) and failureRecords (rejected), plus a bulkSaveSummary count.
HTTP/1.1 200 OK
Content-Type: application/json{
"response": {
"successRecords": [
{
"shortCode": "page-one",
"url": "https://example.com/page-one",
"shortLinkName": "Page One",
"smartLink": "https://re.codelloy.com/page-one",
"isActive": true,
"createdVia": "EXTERNAL",
"lastModifiedVia": "EXTERNAL"
},
{
"shortCode": "a81bb82",
"url": "https://example.com/page-two",
"shortLinkName": "Page Two",
"smartLink": "https://re.codelloy.com/a81bb82",
"isActive": true,
"createdVia": "EXTERNAL",
"lastModifiedVia": "EXTERNAL"
}
],
"failureRecords": [],
"bulkSaveSummary": {
"successCount": 2,
"failureCount": 0
}
}
}Whole-batch validation failure
If the outer payload is malformed (e.g. data missing or empty), the entire request fails with the standard errors[] envelope; no per-record processing happens.
{
"errors": [
{ "code": "SML002", "message": "must not be empty" }
]
}List Short URLs
Returns a paginated list of your short URLs. All filtering is via the query DSL parameter (described below).
GET /link/external/v1/shortenedUrl?page={page}&size={size}&query={query}Request Headers
| Header | Value | Required |
|---|---|---|
X-API-KEY | Your API key | Yes |
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 0 | Page number (zero-based). |
size | integer | 20 | Number of results per page. |
query | string | — | Filter expression in the Codelloy query DSL (see below). |
Example Request
curl -X GET "https://api.codelloy.com/link/external/v1/shortenedUrl?page=0&size=10&query=isActive=true" \
-H "X-API-KEY: your_api_key_here"Success Response
The response envelope wraps a data[] array of short-URL objects plus a page metadata object with size / number / totalElements / totalPages.
HTTP/1.1 200 OK
Content-Type: application/json{
"response": {
"data": [
{
"url": "https://example.com/product/123",
"shortCode": "spring-sale",
"shortLinkName": "Spring sale — product 123",
"smartLink": "https://re.codelloy.com/spring-sale",
"isActive": true,
"cloakEnabled": false,
"enableCustomMetadata": false,
"createdAt": "2026-05-15T10:30:00.742912Z",
"updatedAt": "2026-05-15T10:30:00.742912Z",
"createdBy": "api-key:Production Integration",
"updatedBy": "api-key:Production Integration",
"createdVia": "EXTERNAL",
"lastModifiedVia": "EXTERNAL"
}
],
"page": {
"size": 10,
"number": 0,
"totalElements": 42,
"totalPages": 5
}
}
}
createdAt/updatedAtare ISO-8601 instants in UTC with a trailingZ, at microsecond precision where the value has it (e.g."2026-05-15T10:30:00.742912Z"). Parse them with an instant or offset-aware parser: JavaScriptDate, Pythondatetime.fromisoformat(3.11+), JavaInstant.parse/OffsetDateTime.parse. Request bodies accept both this form and the earlier zone-less form ("2026-05-15T10:30:00").
Query DSL: Filtering Syntax
The query parameter accepts structured expressions in the form field=value. Multiple expressions can be combined with AND / OR.
| Field | Type | Example |
|---|---|---|
url | string | url=https://example.com/page |
shortCode | string | shortCode=spring-sale |
shortLinkName | string | shortLinkName=Spring sale |
smartLink | string | smartLink=https://re.codelloy.com/abc |
isActive | boolean | isActive=true |
enableCustomMetadata | boolean | enableCustomMetadata=true |
cloakEnabled | boolean | cloakEnabled=true |
createdBy | string | createdBy=alice@example.com or createdBy=api-key:Production Integration |
updatedBy | string | Same shape as createdBy. |
createdVia | enum | createdVia=EXTERNAL or createdVia=INTERNAL (case-insensitive). Filter for rows created via the External API or the dashboard. |
lastModifiedVia | enum | Same shape as createdVia; matches the most recent write source. |
Combine expressions with AND (intersection) or OR (union). Whitespace around operators is optional.
# All EXTERNAL-created links that are still active
curl -X GET "https://api.codelloy.com/link/external/v1/shortenedUrl?query=createdVia=EXTERNAL AND isActive=true" \
-H "X-API-KEY: your_api_key_here"# Audit which of your links are cloaked
curl -X GET "https://api.codelloy.com/link/external/v1/shortenedUrl?query=cloakEnabled=true" \
-H "X-API-KEY: your_api_key_here"Unknown enum values return HTTP 400 VE001 with a “Invalid value ‘X’ for enum ChangeSource” message; valid values are INTERNAL and EXTERNAL.
Fetch a Single Link
There is no separate GET-by-id endpoint — and you don’t need one. shortCode is the stable key of a link (the save endpoint upserts on it), and the query DSL matches it with exact equality, so filtering the list endpoint by shortCode returns exactly one row:
curl -X GET "https://api.codelloy.com/link/external/v1/shortenedUrl?query=shortCode=spring-sale" \
-H "X-API-KEY: your_api_key_here"The response is the standard paginated envelope with data containing the single matching link and page.totalElements: 1. If no link with that short code exists in your organisation, you get 200 OK with an empty data: [] — not a 404.
Looking for a link’s click history instead of its configuration? Filter the analytics events endpoint by the same
shortCode.