(PUT) Update/Upsert links
Update existing links or create new ones with upsert operations. Perfect for synchronization scenarios.
Endpoint
PUT /api/links
The PUT endpoint implements an upsert operation, allowing you to update existing links or create new ones in a single request. This is particularly useful for synchronization scenarios where you want to ensure links exist with specific properties.
If an id is provided in the request body, the endpoint updates the existing link with that ID. If the ID does not exist, the request fails. To create new links, omit the id field.
Returns the updated or created links in the same order as the input array, making it easy to correlate requests with responses in bulk operations.
PUT /api/linksAuthentication
Requires authentication with an API key that has the linksWrite scope.
Request Body
The request body must be an array of link objects. Include the id field to update an existing link, or omit it to create a new one.
You can upsert up to 10,000 links in a single request. However, the actual maximum may be lower based on your subscription plan limits and available resources.
| Parameter | Type | Required | Description |
|---|---|---|---|
id (body) | string (UUID) | Yes | Required when updating. Omit to create a new link. If provided, it must exist in your workspace or the request fails. |
url (body) | string (URL) | No | HTTPS destination URL. Required when creating a new link (no id). Optional when updating. |
collectionId (body) | string (UUID) | No | Collection ID. Required when creating a new link (no id). Optional when updating. |
tagIds (body) | string[] | No | Existing tag IDs to assign. When updating, replaces tag assignments if provided. Send `[]` to clear all tags. |
title (body) | string | No | Link title. Only changes when provided. |
description (body) | string | No | Link description. Only changes when provided. |
image (body) | string (URL) | No | HTTPS preview image URL. Only changes when provided. |
utm (body) | object | No | UTM parameters object. Insert entries accept string values; update entries may send null for individual keys to clear them. |
expiresAt (body) | string/null | No | ISO date-time when the link should expire. Send null to clear. Requires link expiration when setting a value. |
expiredUrl (body) | string/null | No | HTTPS destination used after expiration. Send null to clear. Requires link expiration when setting a value. |
password (body) | string/null | No | Password required before resolving the destination. Send null to clear. Requires password protection when set. |
cloaked (body) | boolean/null | No | Render the resolved destination in an iframe. Send false/null to clear. When true, the destination is preflighted and rejected with 400 if it currently blocks iframe rendering. |
ios (body) | string/null | No | HTTPS destination for iOS visitors. Send null to clear. Requires device targeting when setting a value. |
android (body) | string/null | No | HTTPS destination for Android visitors. Send null to clear. Requires device targeting when setting a value. |
geo (body) | object/null | No | Uppercase country-code HTTPS destination map. Send null to clear on updates or omit/null on inserts for no geo targeting. Requires geo targeting when setting destinations. |
externalId (body) | string | No | External ID for syncing. Must be unique in workspace when provided. |
tenantId (body) | string | No | Tenant identifier. Optional. |
partnerId (body) | string (UUID) | No | Partner ID for attribution. Optional. |
Plan Limits
The actual maximum number of links you can upsert per request may be lower than 10,000 based on your subscription plan limits and available resources. The system will enforce both the hard limit (10,000) and your plan-specific limits.
Advanced Link Behavior
password: Visitors must enter the password before Hoko resolves the final destination. A correct password stores a secure, HTTP-only cookie scoped to that short link, so the same visitor can continue without re-entering it.expiresAt: Use an ISO date-time with timezone offset, such as2026-12-31T23:59:00Z. When the current server time is later thanexpiresAt, the link is expired. IfexpiredUrlis set, Hoko sends visitors there. If it is not set, Hoko shows an expired-link page.iosandandroid: Device destinations are selected from the visitor's User-Agent. iOS and Android checks run before geo targeting.geo: Map ISO 3166-1 alpha-2 country codes to destination URLs, for example{ "SA": "https://example.com/sa" }. Use uppercase country codes. Country detection is best-effort and uses trusted edge country headers when available, then IP lookup.cloaked: Hoko renders the resolved destination in a full-page iframe. Create and update requests preflight the active destination and return 400 if it currently blocks iframe rendering.
Advanced fields are applied only when they are set on the link and your workspace plan supports the feature. When updating, include at least one mutable field besides id; omitted fields are left unchanged. Send null to clear nullable fields only on entries that include id, and send false or null to disable cloaked. Entries without id use the same create validation as POST /api/links: nullable clears are not accepted except geo: null for no geo targeting. For tags, omit tagIds to keep existing assignments or send [] to clear them.
All URL fields accepted by this endpoint must use https:. This includes url, image, expiredUrl, ios, android, and every URL value in geo.
Resolution order is: password gate, expiration, iOS/Android targeting, geo targeting, default url, then cloaking. Expired links do not run device or geo targeting.
Examples
curl -X PUT "https://hoko.to/api/links" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://updated.com",
"title": "Updated Link",
"collectionId": "550e8400-e29b-41d4-a716-446655440000",
"tagIds": ["7c4e4a56-f3bb-43e9-9dc7-2f4bda07c8f0"]
}
]'const response = await fetch('https://hoko.to/api/links', {
method: 'PUT',
headers: {
Authorization: 'Bearer <API_KEY>',
'Content-Type': 'application/json'
},
body: JSON.stringify([
{
id: '550e8400-e29b-41d4-a716-446655440000',
url: 'https://updated.com',
title: 'Updated Link',
collectionId: '550e8400-e29b-41d4-a716-446655440000',
tagIds: ['7c4e4a56-f3bb-43e9-9dc7-2f4bda07c8f0']
}
])
});
const links = await response.json();[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://updated.com",
"shortUrl": "https://hoko.to/abc123",
"qrCode": "https://hoko.to/qrcode?text=https%3A%2F%2Fhoko.to%2Fabc123%3Fqr%3D1&size=512&errorCorrection=H&foreground=000000&background=FFFFFF&margin=1&format=svg",
"title": "Updated Link",
"description": null,
"image": null,
"utm": null,
"expiresAt": null,
"expiredUrl": null,
"password": null,
"cloaked": false,
"ios": null,
"android": null,
"geo": null,
"collectionId": "550e8400-e29b-41d4-a716-446655440000",
"tagIds": [
"7c4e4a56-f3bb-43e9-9dc7-2f4bda07c8f0"
],
"externalId": null,
"tenantId": null,
"partnerId": null,
"createdAt": "2024-01-01T00:00:00Z"
}
]Upsert Behavior
When updating, include at least one mutable field besides id. Only the fields you include are changed and omitted fields keep their current values. To clear an optional field, send it explicitly as null on update entries. To replace tag assignments, send tagIds; to clear all tags, send tagIds as an empty array. When creating a new link (no id), url and collectionId are required and create validation matches POST.