---
**📚 Main Documentation:** [Hoko API Documentation (llms.txt)](https://hoko.to/docs/llms.txt)
This is an individual endpoint documentation file. For the complete API reference, see the main documentation above.
---
# (PUT) Update/Upsert links
Update existing links or create new ones with upsert operations. Perfect for synchronization scenarios.
**Category:** Links
## 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.

**Endpoint**

```text
PUT /api/links
```

## Authentication

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 | Location | Description                                                                                                           |
| ------------ | ------------- | -------- | -------- | --------------------------------------------------------------------------------------------------------------------- |
| id           | string (UUID) | Yes      | body     | Required when updating. Omit to create a new link. If provided, it must exist in your workspace or the request fails. |
| url          | string (URL)  | No       | body     | Destination URL. Required when creating a new link (no id). Optional when updating.                                   |
| collectionId | string (UUID) | No       | body     | Collection ID. Required when creating a new link (no id). Optional when updating.                                     |
| title        | string        | No       | body     | Link title. Only changes when provided.                                                                               |
| description  | string        | No       | body     | Link description. Only changes when provided.                                                                         |
| image        | string (URL)  | No       | body     | Preview image URL. Only changes when provided.                                                                        |
| utm          | object        | No       | body     | UTM parameters object. Only the provided UTM keys are updated.                                                        |
| expiresAt    | string/null   | No       | body     | ISO date-time when the link should expire. Send null to clear. Requires link expiration when setting a value.        |
| expiredUrl   | string/null   | No       | body     | Destination used after expiration. Send null to clear. Requires link expiration when setting a value.                |
| password     | string/null   | No       | body     | Password required before resolving the destination. Send null to clear. Requires password protection when set.       |
| cloaked      | boolean/null  | No       | body     | Render the resolved destination in an iframe. Send false/null to clear. Requires link cloaking when true.            |
| ios          | string/null   | No       | body     | Destination for iOS visitors. Send null to clear. Requires device targeting when setting a value.                    |
| android      | string/null   | No       | body     | Destination for Android visitors. Send null to clear. Requires device targeting when setting a value.                |
| geo          | object/null   | No       | body     | Country-code destination map. Send null to clear. Requires geo targeting when setting destinations.                  |
| externalId   | string        | No       | body     | External ID for syncing. Must be unique in workspace when provided.                                                   |
| tenantId     | string        | No       | body     | Tenant identifier. Optional.                                                                                          |
| partnerId    | string (UUID) | No       | body     | Partner ID for attribution. Optional.                                                                                 |

> **Warning: 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.

## Status Codes

### 200

Links updated or created successfully.

**Request**

```bash
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",
      "expiresAt": "2026-12-31T23:59:00Z",
      "expiredUrl": "https://updated.com/expired",
      "cloaked": true,
      "ios": "https://updated.com/ios",
      "android": "https://updated.com/android",
      "geo": {
        "US": "https://updated.com/us"
      },
      "collectionId": "550e8400-e29b-41d4-a716-446655440000"
    }
  ]'
```

**Request**

```javascript
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',
			expiresAt: '2026-12-31T23:59:00Z',
			expiredUrl: 'https://updated.com/expired',
			cloaked: true,
			ios: 'https://updated.com/ios',
			android: 'https://updated.com/android',
			geo: {
				US: 'https://updated.com/us'
			},
			collectionId: '550e8400-e29b-41d4-a716-446655440000'
		}
	])
});
const links = await response.json();
```

**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",
		"expiresAt": "2026-12-31T23:59:00Z",
		"expiredUrl": "https://updated.com/expired",
		"cloaked": true,
		"ios": "https://updated.com/ios",
		"android": "https://updated.com/android",
		"geo": {
			"US": "https://updated.com/us"
		},
		"createdAt": "2024-01-01T00:00:00Z"
	}
]
```

### 400

Invalid request body (missing required fields or invalid values).

**Request**

```bash
curl -X PUT "https://hoko.to/api/links" \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '[
    {
      "url": "invalid-url"
    }
  ]'
```

**Request**

```javascript
const response = await fetch('https://hoko.to/api/links', {
	method: 'PUT',
	headers: {
		Authorization: 'Bearer <API_KEY>',
		'Content-Type': 'application/json'
	},
	body: JSON.stringify([
		{
			url: 'invalid-url'
		}
	])
});
```

**Response**

```json
{
	"error": {
		"en": "Invalid request",
		"ar": "طلب غير صالح"
	}
}
```

### 401

Invalid or missing API key.

**Request**

```bash
curl -X PUT "https://hoko.to/api/links" \
  -H "Authorization: Bearer invalid_key"
```

**Request**

```javascript
const response = await fetch('https://hoko.to/api/links', {
	method: 'PUT',
	headers: {
		Authorization: 'Bearer invalid_key'
	}
});
```

**Response**

```json
{
	"error": {
		"en": "Invalid API key",
		"ar": "مفتاح API غير صالح"
	}
}
```

### 403

API key does not have the required linksWrite scope.

**Request**

```bash
curl -X PUT "https://hoko.to/api/links" \
  -H "Authorization: Bearer <API_KEY>"
```

**Request**

```javascript
const response = await fetch('https://hoko.to/api/links', {
	method: 'PUT',
	headers: {
		Authorization: 'Bearer <API_KEY>'
	}
});
```

**Response**

```json
{
	"error": {
		"en": "Missing required scopes",
		"ar": "الصلاحيات المطلوبة مفقودة"
	},
	"missingScopes": ["linksWrite"]
}
```

### 429

Rate limit exceeded. Check X-RateLimit-\* headers for details.

**Request**

```bash
curl -X PUT "https://hoko.to/api/links" \
  -H "Authorization: Bearer <API_KEY>"
```

**Request**

```javascript
const response = await fetch('https://hoko.to/api/links', {
	method: 'PUT',
	headers: {
		Authorization: 'Bearer <API_KEY>'
	}
});
```

**Response**

```json
{
	"error": {
		"en": "Rate limit exceeded",
		"ar": "تم تجاوز حد المعدل"
	},
	"retryAfter": 60
}
```

## Examples

**Request**

```bash
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"
    }
  ]'
```

**Request**

```javascript
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'
		}
	])
});
const links = await response.json();
```

**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",
		"collectionId": "550e8400-e29b-41d4-a716-446655440000",
		"createdAt": "2024-01-01T00:00:00Z"
	}
]
```

> **Info: Upsert Behavior**
> When updating, only the fields you include are changed and omitted fields keep their current values. To clear an optional field, send it explicitly as null. When creating a new link (no id), url and collectionId are required.

---

**Back to main documentation:** [Hoko API Documentation (llms.txt)](https://hoko.to/docs/llms.txt)