Чтобы сократить URL через API, отправьте один аутентифицированный POST: положите длинный URL в JSON-тело как destination_url, отправьте его на https://api.elido.app/v1/links с заголовком Authorization: Bearer <ваш ключ> и прочитайте short_url из ответа 201 Created. Это вся интеграция на любом языке. Восемь языков ниже различаются лишь тем, как записывают запрос. И тем, какая продакшен-мелочь ударит первой.
Первый вызов занимает пять минут. Проблемы начинаются позже, когда приходит продакшен-трафик и обработчик повторов создаёт дубликаты: дашборд заполняется вариантами /foo-1, /foo-2 и /foo-3 для одного адреса назначения, и кто-то заводит тикет. Это руководство - разбор API сокращателя ссылок, который я хотел бы дать той команде заранее: аутентификация, первый вызов, четыре конечные точки ссылок, ключи идемпотентности, коды ошибок, лимиты запросов, а затем тот же запрос на создание короткой ссылки на восьми языках с одной привычкой, нужной каждой среде выполнения.
Ещё выбираете сервис? Сравнение бесплатных API сокращателей сопоставляет форму запросов и бесплатные лимиты разных поставщиков. Всё ниже относится к REST API Elido.
Сразу к вашему языку:
- Python:
requestsдля одной ссылки,httpxс семафором для тысяч. - JavaScript и Node.js:
fetch, почему ключ не должен попадать в браузер, пул воркеров вместоPromise.all. - Go: один общий
http.Clientс тайм-аутом,errgroup.SetLimitдля пакетов. - Java:
HttpClientиз JDK, виртуальные потоки заSemaphore. - C# и .NET:
PostAsJsonAsync, затем типизированный клиент отIHttpClientFactory. - PHP, Laravel и WordPress: curl,
Http::с повторами,wp_remote_postпри публикации. - Ruby:
Net::HTTP.post, затем Faraday с промежуточным слоем повторов, который действительно повторяет POST. - PowerShell:
Invoke-RestMethod, ключ в хранилище секретов и целая колонка CSV.
Предварительные требования: API-ключ и базовый URL
Войдите в дашборд, откройте /dashboard/api-keys и создайте API-ключ. Ключи начинаются с elido_ и показываются один раз. Каждый ключ привязан к одному рабочему пространству, поэтому ключ из пространства A не создаст ссылку в пространстве B, а API определяет, какое пространство вы имеете в виду, только по ключу. Токены машинных пользователей (для CI-систем, внутренних инструментов и интеграций machine-to-machine) живут на /dashboard/machine-users и ротируются независимо от персональных ключей.
Оба вида несут заранее заданную роль в рабочем пространстве (viewer, editor или admin), а не права для отдельных конечных точек, поэтому CI-задача, которая только создаёт ссылки, получает editor. Руководство по правам API-ключей перечисляет, до чего дотягивается каждая роль. Изменения вебхуков требуют admin, и там объяснено почему.
Базовый URL - https://api.elido.app, а маршруты ссылок находятся под /v1. Домены редиректа (f.elido.me на Free, s.elido.me на платных тарифах, b.elido.me на Business) - отдельная поверхность: ваши короткие ссылки работают именно там, а API их создаёт и правит. Спецификация OpenAPI 3.1, на которой держится справочник API, и есть контракт. Если поле есть в спецификации, оно поддерживается.
Одна мелочь ловит новые аккаунты. Запись с ключом, владелец которого не подтвердил почту, возвращает 403 (email_not_verified). Чтение при этом работает. Токены машинных пользователей освобождены от этого правила. Подтвердите адрес один раз, и ошибка уйдёт.
Первый вызов: создание короткой ссылки
Вот запрос, который воспроизводит каждый языковой раздел ниже, в curl:
curl -sS https://api.elido.app/v1/links \
-H "Authorization: Bearer $ELIDO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"destination_url": "https://shop.example.com/spring-sale"}'
Ответ - 201 Created с записью ссылки. Сокращённо, до нужных вам полей:
{
"id": 48213,
"workspace_id": 42,
"domain_id": 3,
"slug": "aB3xK9",
"destination_url": "https://shop.example.com/spring-sale",
"tags": [],
"redirect_status": 302,
"password_set": false,
"short_url": "https://s.elido.me/aB3xK9",
"created_at": "2026-11-11T09:14:03Z"
}
Сохраните id, если когда-нибудь будете править, завершать или удалять ссылку: это канонический идентификатор, а слаги могут меняться, ID - нет. Вы не передавали domain_id, поэтому API взял короткий домен вашего тарифа по умолчанию. Слаг генерирует сервер, если вы не пришлёте slug; сгенерированные слаги состоят из base62 ([0-9A-Za-z]) и не длиннее шести символов.
Держите ключ в переменной окружения (ELIDO_API_KEY во всех примерах здесь), никогда в исходниках, потому что ключ сокращателя - это платёжный доступ: любой, у кого он есть, может создавать ссылки за вашу квоту, и острее всего это в JavaScript, как объясняет раздел про Node.
Четыре конечные точки, которыми вы действительно будете пользоваться
В API конечных точек больше четырёх, но большинство интеграций не выходят за этот набор. Плоские маршруты /v1/links определяют рабочее пространство по вашему ключу; канонические пути под /v1/workspaces/{workspace_id}/links ведут себя так же, но требуют явного ID.
Создание ссылки
POST /v1/links принимает destination_url и необязательные поля:
slug- хвост адреса, который вы выбираете; он должен быть уникален на домене.domain_id- для собственного домена; опустите, чтобы получить короткий домен вашего тарифа по умолчанию.titleиtags- подпись в дашборде и произвольные строки, по которым потом можно фильтровать.expires_at- метка времени по RFC 3339, после которой ссылка перестаёт перенаправлять и показывает страницу об истёкшем сроке.redirect_status-301,302(по умолчанию) или307.
Пароли при создании не задаются: отправьте PATCH сразу после, и редирект покажет страницу с паролем перед переходом. UTM-параметры идут прямо в destination_url, а ваши собственные ключи для связывания - в tags.
Собственный слаг - то поле, которое кусает команды в продакшене. Повторите слаг на том же домене - и получите 409. Наивный обработчик, который добавляет счётчик (my-slug-1, my-slug-2), - ровно так начинается путаница с дубликатами из вступления. Исправление - ключ идемпотентности, о нём через два раздела.
Чтение и список ссылок
GET /v1/links/{id} возвращает полную запись, включая short_url. Счётчиков кликов в записи ссылки нет. Они приходят из конечных точек аналитики, описанных в руководстве по API аналитики кликов.
GET /v1/links?limit=…&tags=…&host=… возвращает список ссылок с фильтрами (q ищет, status и folder сужают дальше). Пагинация курсорная: отправьте next_cursor из ответа обратно как cursor и остановитесь, когда он равен null.
Обновление ссылки
PATCH /v1/links/{id} принимает те же поля, что и создание. Типичные обновления - новый destination_url (ротация кампании без перепечатки QR-кода), новые теги и более поздний expires_at. Отправка нового slug переименовывает ссылку. Старый слаг умирает сразу. Не переименовывайте то, что уже напечатано.
Удаление и восстановление ссылки
DELETE /v1/links/{id} возвращает 204 No Content и переносит ссылку в корзину: она перестаёт перенаправлять и исчезает из списков и вызовов чтения. Но окончательно её ещё не удалили. POST /v1/links/{id}/restore возвращает ссылку, а GET /v1/links/trash показывает содержимое корзины. Через 30 дней корзина очищается насовсем.
Ключи идемпотентности: повторы без дубликатов ссылок
Каждый POST, PUT и PATCH принимает заголовок Idempotency-Key - непрозрачную строку по вашему выбору. Когда ключ впервые приходит с успешным ответом, сервер хранит код состояния и тело 24 часа в пределах пары (рабочее пространство, ключ). Повтор того же ключа с тем же запросом возвращает сохранённый ответ, так что повторная попытка отдаёт уже существующую ссылку, а не создаёт вторую.
От трёх деталей зависит, сработает ли это у вас:
- Хранятся только успешные ответы.
429или500не кэшируются, так что следующая попытка с тем же ключом получит настоящий второй шанс. - Сервер снимает отпечаток метода, пути и тела. Используйте ключ с другим телом - и получите
422сidempotency_key_mismatch; это ошибка вызывающего, а не повод повторять. DELETEключ не принимает; двойное удаление и так идемпотентно.
Сам ключ должен происходить из чего-то стабильного. Выводите его из бизнес-события (order-12345-link), когда оно есть, или из SHA-256 хеша адреса назначения в пакетной задаче. Случайный UUID на каждую попытку лишает смысла всю затею. Один ключ на URL работает, пока пакет не запустят второй раз.
Подвох - в 24-часовом окне. Повтор на третий день застрявшей задачи создаст новую ссылку, поэтому в многодневных пакетах сохраните id из первого успешного создания и ищите его перед повторной попыткой. Ключи действуют и в пределах рабочего пространства: один и тот же ключ в двух пространствах создаёт две ссылки, что верно для API с несколькими пространствами и всё равно удивляет команды, считавшие ключи глобальными.
У ключа из хеша адреса есть побочный эффект, о котором стоит знать. Две кампании с одним URL в течение 24 часов делят одну ссылку. Если нужны две ссылки, добавьте в ключ ID кампании.
Обработка ошибок и коды состояния
Ошибки приходят как небольшой JSON-объект с понятным человеку error и, местами, машиночитаемым code:
{ "error": "destination_url is required" }
Коды состояния, которые вам встретятся:
400- ошибка валидации: отсутствуетdestination_url, некорректныйexpires_at, недопустимыйredirect_status. Исправьте тело запроса; не повторяйте.401- ключа нет, он отозван или набран с опечаткой. Проверьте, что в заголовке послеBearerстоит пробел перед ключом.402- квота ссылок вашего тарифа исчерпана. Повтор не поможет; поможет переход на другой тариф или ожидание следующего периода.403- роль ключа не позволяет это действие (ключviewerне создаёт ссылки), почта аккаунта не подтверждена или адрес назначения заблокирован проверкой безопасности ("code": "destination_blocked").404- ссылки не существует или этот ключ её не видит. Мы возвращаем 404, а не 403, чтобы вызывающий не мог выведывать существование чужих ресурсов.409- слаг на этом домене занят.422- ключ идемпотентности использован с другим телом или указана недопустимая комбинация опций.429- вы упёрлись в лимит запросов. ПодождитеRetry-Afterи повторите с тем же ключом идемпотентности.500,502,503,504- серверный или временный сбой. Сделайте паузу и повторите с тем же ключом.
Это разделение и есть вся политика повторов, и каждый языковой раздел ниже реализует её одинаково: повторять 429 и 5xx, на остальном сдаваться. Повтор 400 с тем же телом не даст другого ответа, а три повтора 401 - просто более медленный сбой. Руководство о лимитах запросов и повторах глубже разбирает джиттер и то, почему слепые циклы повторов превращают один сбой в два.
Лимиты запросов и пакетная конечная точка
Трафик API измеряется для каждого рабочего пространства по алгоритму token bucket:
| Тариф | Постоянная скорость | Всплеск |
|---|---|---|
| Free | 10 запросов/с | 20 |
| Pro | 30 запросов/с | 60 |
| Business | 100 запросов/с | 200 |
Полное ведро позволяет отправить всплеск разом. Затем вы переходите на постоянную скорость. Помимо лимита пространства, у каждого API-ключа есть собственное ведро, так что один сбежавший скрипт не истратит весь запас рабочего пространства в одиночку.
Отклонённый запрос получает 429, заголовок Retry-After: 1 и заголовок X-RateLimit-Scope, указывающий, какое именно ведро опустело (workspace или api_key). Заголовков с остатком квоты, по которым можно было бы выдерживать темп, нет. Поэтому практичный подход - жёсткий потолок параллельности плюс повтор на 429: восемь запросов в полёте - удобное начало на Pro, а для Free лучше четыре. Одна странность застала меня врасплох. Тело 429 - это JSON-подобный текст, отправленный как text/plain, поэтому клиент, разбирающий только JSON-типы содержимого, получит обычную строку вместо ожидаемого объекта.
Большое задание? POST /v1/links/bulk принимает {"links": [...]} до 100 элементов и стоит один запрос в лимите. Он отвечает 200, даже когда часть элементов провалилась, со счётчиками ok и failed и статусом ok или error у каждого элемента, так что читайте каждый. Не пропускайте этого. Возвращённые ссылки содержат slug, но не short_url, то есть URL вы собираете сами из вашего домена и слага. Пошаговый разбор массового импорта из Google Sheets переносит так целую таблицу.
Сокращение URL на вашем языке: общая схема
Каждый раздел дальше отправляет запрос из первого вызова и придерживается тех же четырёх привычек: ключ из переменной окружения, тайм-аут на каждый запрос, проверка кода состояния до разбора тела и стабильный Idempotency-Key на всё, что может выполниться дважды. Я не буду объяснять их заново для каждого языка. Меняется только идиоматика. У каждой среды выполнения есть ещё и одна деталь, в которой люди ошибаются.
Каждый раздел открывается минимальным рабочим вызовом. Этого хватает для скрипта, который вы запускаете вручную. Дальше идёт продакшен-часть, о которой пользователи этой среды спрашивают чаще всего: параллельность в Python, Go или Java; время жизни клиента в C#; хостинг в PHP; промежуточные слои в Ruby; секреты и CSV в PowerShell. Хотите запустить всё как есть? Создайте ключ на бесплатном тарифе, экспортируйте его как ELIDO_API_KEY, и каждый фрагмент заработает без изменений.
Сокращение URL в Python
В Python сокращение URL - это один requests.post с ключом в заголовке Bearer, адресом назначения в json= и тайм-аутом:
import os
import requests
resp = requests.post(
"https://api.elido.app/v1/links",
headers={"Authorization": f"Bearer {os.environ['ELIDO_API_KEY']}"},
json={"destination_url": "https://example.com/a-very-long-path?with=params"},
timeout=10,
)
resp.raise_for_status()
print(resp.json()["short_url"]) # https://s.elido.me/aB3xK9
У библиотеки requests нет тайм-аута по умолчанию, поэтому без timeout=10 зависшее соединение блокирует скрипт навсегда. raise_for_status() превращает 401 или 500 в исключение, чтобы неудачный вызов не выглядел успешным. Если вы ничего не можете установить через pip install, urllib.request из стандартной библиотеки отправит тот же POST, только с большей церемонией.
Пакеты с httpx и семафором
Цикл по requests.post годится для нескольких сотен ссылок. Для десятков тысяч время уходит на ожидание сети, а httpx с asyncio накладывает эти ожидания друг на друга. Важнее всего здесь семафор. Он ограничивает число запросов в полёте, и именно это отличает быстрый пакет от стены 429, из которой потом приходится выбираться циклом повторов.
import asyncio
import hashlib
import os
import httpx
API = "https://api.elido.app/v1/links"
async def shorten_all(urls: list[str], concurrency: int = 8) -> dict[str, str]:
gate = asyncio.Semaphore(concurrency)
headers = {"Authorization": f"Bearer {os.environ['ELIDO_API_KEY']}"}
async with httpx.AsyncClient(headers=headers, timeout=10) as client:
async def one(url: str) -> tuple[str, str]:
key = hashlib.sha256(url.encode()).hexdigest() # same key on every retry
async with gate:
for attempt in range(3):
r = await client.post(
API,
json={"destination_url": url},
headers={"Idempotency-Key": key},
)
if r.status_code == 429 or r.status_code >= 500:
await asyncio.sleep(int(r.headers.get("Retry-After", 2**attempt)))
continue
if r.is_success:
return url, r.json()["short_url"]
return url, f"ERROR {r.status_code}: {r.text}"
return url, "ERROR: gave up after 3 attempts"
pairs = await asyncio.gather(*(one(u) for u in urls))
return dict(pairs)
# results = asyncio.run(shorten_all(my_urls))
Результаты индексированы исходным URL. Частичный сбой остаётся видимым. Поскольку ключ - хеш адреса назначения, повторный запуск всего списка после исправления трёх плохих строк ничего не создаст дважды. Сон внутри семафора - сознательный выбор: воркер, которому велели отступить, не должен отдавать свой слот новому запросу.
Сокращение URL в JavaScript и Node.js
В JavaScript сокращение URL - это один POST через fetch. В Node fetch глобален с версии 18, так что ничего устанавливать не нужно:
const res = await fetch("https://api.elido.app/v1/links", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ELIDO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ destination_url: "https://example.com/spring-sale" }),
signal: AbortSignal.timeout(10_000),
});
if (!res.ok)
throw new Error(`shorten failed: ${res.status} ${await res.text()}`);
const { short_url } = await res.json();
console.log(short_url);
Запустите его командой node --env-file=.env shorten.mjs. Строка if (!res.ok) - не украшение: fetch отклоняется только при сетевом сбое или отмене, поэтому 401 приходит как выполненный промис с ok: false. Пользователи Axios на этом обжигаются однажды. Обычно это проявляется как undefined там, где должна быть ссылка.
Держите ключ на сервере
Вставьте этот фрагмент в React-компонент - и сломаются две вещи. CORS заблокирует ответ, потому что API с аутентификацией по ключу не отправляет Access-Control-Allow-Origin для произвольных сайтов. Проблема побольше прячется за ошибкой: ключ во фронтенд-коде может прочитать любой, кто откроет DevTools, а префикс NEXT_PUBLIC_ выставляет его напоказ. Поэтому браузер отправляет на ваш маршрут обычный URL. Ключ добавляет маршрут.
Проверяйте URL, который принимает ваш маршрут, иначе вы построили открытый редирект с лишними шагами; обзор уязвимостей открытого перенаправления показывает, что с таким делают злоумышленники.
Для пакета не полагайтесь на Promise.all(urls.map(shorten)). Он запускает все запросы в одном такте, поэтому 500 URL превращаются в 500 одновременных соединений, и большинство возвращается с 429. Небольшой пул воркеров, берущих из общей очереди, держит параллельность ровной, какой бы длинный ни был список:
import { createHash } from "node:crypto";
async function shorten(url) {
const key = createHash("sha256").update(url).digest("hex"); // stable across retries
for (let attempt = 0; attempt < 3; attempt++) {
const res = await fetch("https://api.elido.app/v1/links", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ELIDO_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": key,
},
body: JSON.stringify({ destination_url: url }),
signal: AbortSignal.timeout(10_000),
});
if (res.status === 429 || res.status >= 500) {
const wait = Number(res.headers.get("Retry-After") ?? 2 ** attempt);
await new Promise((r) => setTimeout(r, wait * 1000));
continue;
}
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
return (await res.json()).short_url;
}
throw new Error("gave up after 3 attempts");
}
export async function shortenAll(urls, concurrency = 8) {
const queue = [...urls];
const results = new Map();
const worker = async () => {
while (queue.length) {
const url = queue.shift();
try {
results.set(url, await shorten(url));
} catch (err) {
results.set(url, `ERROR: ${err.message}`); // one bad URL must not sink the batch
}
}
};
await Promise.all(Array.from({ length: concurrency }, worker));
return results;
}
В edge-средах (Vercel Edge, Cloudflare Workers, Deno) node:crypto может отсутствовать; хешируйте через crypto.subtle.digest("SHA-256", …), а остальное остаётся без изменений.
Сокращение URL в Go
В Go сокращение URL требует net/http и encoding/json из стандартной библиотеки: сериализовать тело, выставить два заголовка, разобрать short_url. С самого начала напишите это как функцию на общем клиенте, потому что именно эту версию вы и оставите:
var client = &http.Client{Timeout: 10 * time.Second}
func shorten(ctx context.Context, destination string) (string, error) {
body, err := json.Marshal(map[string]string{"destination_url": destination})
if err != nil {
return "", err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
"https://api.elido.app/v1/links", bytes.NewReader(body))
if err != nil {
return "", err
}
sum := sha256.Sum256([]byte(destination))
req.Header.Set("Authorization", "Bearer "+os.Getenv("ELIDO_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", hex.EncodeToString(sum[:]))
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("post: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
msg, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
return "", fmt.Errorf("shorten %s: %s %s", destination, resp.Status, msg)
}
var link struct {
ShortURL string `json:"short_url"`
}
if err := json.NewDecoder(resp.Body).Decode(&link); err != nil {
return "", fmt.Errorf("decode: %w", err)
}
return link.ShortURL, nil
}
У http.DefaultClient и http.Post тайм-аута нет вообще. Совсем. Не отвечающий хост может удерживать горутину минутами. Один клиент уровня пакета сохраняет пул соединений между вызовами, а NewRequestWithContext означает, что отменённый CLI или отключившийся вызывающий останавливает запрос, а не оставляет его работать. Если пакет открывает намного больше сокетов, чем вы ожидали, поднимите MaxIdleConnsPerHost в транспорте: его значение по умолчанию 2 незаметно в скрипте и дорого, когда восемь горутин делят один хост.
Ограниченное разветвление с errgroup.SetLimit
Одна горутина на URL для Go дёшева, а для лимита запросов разорительна: 5000 URL превращаются в 5000 запросов сразу. errgroup с SetLimit даёт разветвление с потолком.
func shortenAll(ctx context.Context, urls []string) map[string]string {
var (
g errgroup.Group
mu sync.Mutex
out = make(map[string]string, len(urls))
)
g.SetLimit(8) // at most 8 requests in flight, whatever len(urls) is
for _, u := range urls {
g.Go(func() error {
short, err := shorten(ctx, u)
if err != nil {
short = "ERROR: " + err.Error()
}
mu.Lock()
out[u] = short
mu.Unlock()
return nil
})
}
_ = g.Wait()
return out
}
Здесь намеренно обычный errgroup.Group. errgroup.WithContext отменяет всё после первой ошибки, что подходит сборке по принципу «всё или ничего» и совершенно не подходит импортёру, который должен сократить всё, что может, пройти мимо плохих строк и отчитаться о сбоях в конце. Начиная с Go 1.22 переменная цикла своя на каждой итерации, поэтому старая строка u := u ушла, но мьютекс не опционален: go test -race пометит незащищённую мапу. Добавьте повтор на 429 и 5xx внутри shorten, использовав select на ctx.Done() и time.After, чтобы ожидание можно было прервать.
Сокращение URL в Java
В Java сокращение URL - это один POST через java.net.http.HttpClient, который есть в JDK с Java 11, так что первой версии не нужна зависимость Maven:
private static final HttpClient CLIENT = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
public static void main(String[] args) throws Exception {
HttpRequest request = HttpRequest
.newBuilder(URI.create("https://api.elido.app/v1/links"))
.header("Authorization", "Bearer " + System.getenv("ELIDO_API_KEY"))
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(10))
.POST(HttpRequest.BodyPublishers.ofString(
"{\"destination_url\":\"https://example.com/spring-sale\"}"))
.build();
HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 300) {
throw new IllegalStateException(response.statusCode() + " " + response.body());
}
System.out.println(response.body()); // JSON with "short_url"
}
Два тайм-аута делают две работы. connectTimeout ограничивает рукопожатие TCP и TLS; timeout запроса ограничивает весь обмен, и без него сервер, принявший соединение и замолчавший, удерживает поток. send бросает исключение только при транспортных сбоях и тайм-аутах, никогда при HTTP-ошибке, поэтому проверка кода состояния здесь является контрактом, а не лишней осторожностью. Создайте один HttpClient на приложение: он потокобезопасен и владеет пулом соединений, а создание нового на каждый вызов каждый раз повторяет рукопожатие TLS.
Виртуальные потоки за семафором
Виртуальные потоки стали окончательными в Java 21. Они делают поток на каждый URL достаточно дешёвым, чтобы это имело смысл. Но лимит запросов никуда не делся, поэтому настоящую работу выполняет Semaphore. Разбирайте ответ Jackson-ом. Строка JSON, собранная вручную, ломается на первом же адресе назначения с кавычкой.
static final ObjectMapper MAPPER = new ObjectMapper();
static String shorten(String destination) throws Exception {
String key = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
.digest(destination.getBytes(StandardCharsets.UTF_8)));
HttpRequest request = HttpRequest
.newBuilder(URI.create("https://api.elido.app/v1/links"))
.header("Authorization", "Bearer " + System.getenv("ELIDO_API_KEY"))
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.timeout(Duration.ofSeconds(10))
.POST(HttpRequest.BodyPublishers.ofString(
MAPPER.writeValueAsString(Map.of("destination_url", destination))))
.build();
HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 300) {
throw new IllegalStateException(response.statusCode() + " " + response.body());
}
return MAPPER.readTree(response.body()).get("short_url").asText();
}
static Map<String, String> shortenAll(List<String> urls) {
Map<String, String> out = new ConcurrentHashMap<>();
Semaphore gate = new Semaphore(8); // 8 in flight, whatever urls.size() is
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (String url : urls) {
executor.submit(() -> {
gate.acquire();
try {
out.put(url, shorten(url));
} catch (Exception e) {
out.put(url, "ERROR: " + e.getMessage());
} finally {
gate.release();
}
return null;
});
}
} // close() waits for every task
return out;
}
Блок try-with-resources выполняет настоящую работу: закрытие исполнителя виртуальных потоков ждёт каждую поданную задачу, так что никакой возни с awaitTermination не нужно. На Java 11-17 та же схема работает с sendAsync и пулом фиксированного размера. Не работает ни на одной версии CompletableFuture.allOf по неограниченному списку: он запускает всё сразу и собирает кучу 429.
Сокращение URL в C# и .NET
В C# сокращение URL - это один вызов PostAsJsonAsync на HttpClient, который уже несёт заголовок Bearer:
using System.Net.Http.Json;
using System.Text.Json.Serialization;
var http = new HttpClient
{
BaseAddress = new Uri("https://api.elido.app"),
Timeout = TimeSpan.FromSeconds(10),
};
http.DefaultRequestHeaders.Authorization =
new("Bearer", Environment.GetEnvironmentVariable("ELIDO_API_KEY"));
var response = await http.PostAsJsonAsync("/v1/links",
new { destination_url = "https://example.com/spring-sale" });
response.EnsureSuccessStatusCode();
var link = await response.Content.ReadFromJsonAsync<LinkResponse>();
Console.WriteLine(link!.ShortUrl);
record LinkResponse([property: JsonPropertyName("short_url")] string ShortUrl);
EnsureSuccessStatusCode - та самая строка, которая имеет значение. Без неё 401 завершается благополучно, ReadFromJsonAsync возвращает запись, полную null, а сбой всплывает тремя слоями дальше как NullReferenceException. Анонимный объект сохраняет имя поля в snake_case без политики именования; в настоящем проекте один раз задайте JsonNamingPolicy.SnakeCaseLower в опциях сериализатора.
Типизированный клиент от IHttpClientFactory
Этот фрагмент хорош в консольном эксперименте и неверен в сервисе. HttpClient, созданный на каждый вызов, каждый раз открывает новый пул соединений, а его освобождение оставляет сокеты в TIME_WAIT; делайте так в цикле - и порты закончатся. IHttpClientFactory объединяет обработчики в пул и ротирует их, чтобы подхватывать изменения DNS.
builder.Services.AddHttpClient<ElidoClient>(client =>
{
client.BaseAddress = new Uri("https://api.elido.app");
client.DefaultRequestHeaders.Authorization =
new("Bearer", builder.Configuration["Elido:ApiKey"]);
})
.AddStandardResilienceHandler(); // retries, timeouts, circuit breaker
public sealed class ElidoClient(HttpClient http, ILogger<ElidoClient> logger)
{
public async Task<string> ShortenAsync(string destination, CancellationToken ct = default)
{
using var request = new HttpRequestMessage(HttpMethod.Post, "/v1/links")
{
Content = JsonContent.Create(new { destination_url = destination }),
};
// stable across retries, so the resilience handler can't create duplicates
request.Headers.Add("Idempotency-Key",
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(destination))));
using var response = await http.SendAsync(request, ct);
if (!response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync(ct);
logger.LogWarning("shorten failed {Status}: {Body}", (int)response.StatusCode, body);
throw new HttpRequestException($"shorten failed: {(int)response.StatusCode} {body}");
}
var link = await response.Content.ReadFromJsonAsync<LinkResponse>(ct);
return link!.ShortUrl;
}
}
AddStandardResilienceHandler приходит из Microsoft.Extensions.Http.Resilience и приносит повторы с джиттером на 429 и 5xx, тайм-ауты и предохранитель-размыкатель. Он будет повторять POST, если не запретить, и именно заголовок Idempotency-Key делает это безопасным. Вторая половина - чтение тела ошибки перед выбросом исключения: EnsureSuccessStatusCode оставляет только код состояния, и в три часа ночи вы гадаете, чем именно 400 была недовольна. Для пакета Parallel.ForEachAsync с MaxDegreeOfParallelism = 8 ограничивает запросы в полёте и читается лучше самодельного семафора. Локально держите ключ в user secrets, а в продакшене - в переменных окружения или хранилище секретов, никогда в закоммиченном appsettings.json.
Сокращение URL в PHP, Laravel и WordPress
В PHP сокращение URL работает через расширение curl, которое поставляется с PHP, поэтому запускается даже на виртуальном хостинге, где Composer недоступен:
<?php
$ch = curl_init('https://api.elido.app/v1/links');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('ELIDO_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['destination_url' => 'https://example.com/spring-sale']),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($body === false || $status >= 300) {
throw new RuntimeException("shorten failed: HTTP {$status} {$body}");
}
echo json_decode($body, true, 512, JSON_THROW_ON_ERROR)['short_url'];
Две строки существуют из-за ошибок, которые каждый ловит однажды. Без CURLOPT_RETURNTRANSFER curl печатает JSON в вывод и возвращает true, и это настоящая история за большинством вопросов «мой ответ пустой». А CURLOPT_POSTFIELDS переключается на multipart-кодирование формы, как только получает массив, поэтому именно json_encode делает запрос JSON-запросом. Многие PHP-руководства до сих пор вызывают urlshortener/v1 от Google или конечную точку bit.ly v3; обе давно исчезли, а статья об альтернативе Google URL Shortener рассказывает, куда делись ссылки goo.gl.
HTTP-клиент Laravel с повторами
В приложении на Laravel или Symfony HTTP-клиент Laravel (обёртка над Guzzle) объявляет политику повторов один раз:
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.elido.key'))
->withHeaders(['Idempotency-Key' => hash('sha256', $destination)])
->timeout(10)
->retry(3, 1000, function (Throwable $e) {
return $e instanceof ConnectionException
|| ($e instanceof RequestException
&& in_array($e->response->status(), [429, 500, 502, 503, 504], true));
}, throw: false)
->post('https://api.elido.app/v1/links', ['destination_url' => $destination]);
$short = $response->successful() ? $response->json('short_url') : null;
Колбэк when не даёт повторять 401 трижды. Чистый Guzzle ведёт себя иначе, чем curl: по умолчанию он бросает ClientException на 4xx и ServerException на 5xx, так что перехватывайте их или отключите http_errors. Для ночного импорта Pool Guzzle с 'concurrency' => 8 - это ответ PHP на ограниченную параллельность. Одна ловушка, свойственная PHP: sleep() внутри веб-запроса всё это время удерживает воркер php-fpm, поэтому повторам место в CLI-скриптах, очередях задач и WP-Cron.
WordPress: wp_remote_post при публикации
В WordPress есть собственный HTTP-слой wp_remote_post, который сам выбирает транспорт и работает на хостингах с отключённым curl. Привяжите его к публикации и сохраните результат как метаданные записи:
add_action('publish_post', function (int $post_id): void {
if (get_post_meta($post_id, '_elido_short_url', true)) {
return; // already shortened
}
$response = wp_remote_post('https://api.elido.app/v1/links', [
'timeout' => 10,
'headers' => [
'Authorization' => 'Bearer ' . ELIDO_API_KEY,
'Content-Type' => 'application/json',
'Idempotency-Key' => 'post-' . $post_id,
],
'body' => wp_json_encode(['destination_url' => get_permalink($post_id)]),
]);
if (is_wp_error($response) || wp_remote_retrieve_response_code($response) >= 300) {
error_log('shorten failed for post ' . $post_id);
return;
}
$link = json_decode(wp_remote_retrieve_body($response), true);
update_post_meta($post_id, '_elido_short_url', $link['short_url']);
});
Определите ELIDO_API_KEY в wp-config.php, а не в файле плагина, который окажется в публичном репозитории, и не в строке опций, которую видит каждый администратор. Ключ post-123 стабилен. Сработай хук дважды - и второй вызов вернёт существующую ссылку. Если вы вообще не хотите поддерживать хук, руководство по сокращению ссылок в WordPress сравнивает этот путь с плагином и бескодовыми вариантами.
Сокращение URL в Ruby
В Ruby сокращение URL - это один Net::HTTP.post из стандартной библиотеки, так что скрипту не нужен ни один gem:
require "json"
require "net/http"
response = Net::HTTP.post(
URI("https://api.elido.app/v1/links"),
{ destination_url: "https://example.com/spring-sale" }.to_json,
"Authorization" => "Bearer #{ENV.fetch('ELIDO_API_KEY')}",
"Content-Type" => "application/json",
)
raise "shorten failed: #{response.code} #{response.body}" unless response.is_a?(Net::HTTPSuccess)
puts JSON.parse(response.body)["short_url"]
ENV.fetch вместо ENV[] падает на месте, когда ключа нет, а не отправляет Bearer и возвращает запутанный 401. Проверку is_a?(Net::HTTPSuccess) люди пропускают чаще всего: Net::HTTP бросает исключение на мёртвых сокетах и тайм-аутах и больше ни на что. Net::HTTP.post также не принимает аргументов тайм-аута, поэтому для всего, что работает без присмотра, перейдите на Net::HTTP.start с open_timeout и read_timeout.
Faraday с промежуточным слоем повторов
Внутри Rails-приложения Faraday даёт один объект соединения с прикреплённым заголовком аутентификации, JSON в обе стороны и объявленной политикой повторов. Начиная с Faraday 2 промежуточный слой повторов живёт в отдельном gem faraday-retry.
# Gemfile: gem "faraday" and gem "faraday-retry"
require "digest"
require "faraday"
require "faraday/retry"
ELIDO = Faraday.new(url: "https://api.elido.app") do |f|
f.request :json
f.request :retry, max: 3, interval: 0.5, backoff_factor: 2,
methods: %i[post], # POST is not retried by default
retry_statuses: [429, 500, 502, 503, 504]
f.response :json, content_type: /\bjson$/
f.headers["Authorization"] = "Bearer #{ENV.fetch('ELIDO_API_KEY')}"
f.options.timeout = 10
end
def shorten(destination)
res = ELIDO.post("/v1/links", { destination_url: destination },
"Idempotency-Key" => Digest::SHA256.hexdigest(destination))
raise "shorten failed: #{res.status} #{res.body}" unless res.success?
res.body["short_url"]
end
Строка methods: %i[post] - та, которую большинство сниппетов Faraday в интернете опускает. Из коробки faraday-retry повторяет только идемпотентные глаголы (GET, PUT, DELETE, HEAD, OPTIONS), поэтому конфигурация для POST без неё не повторяет ничего, и никто этого не замечает, пока 503 не потеряет ссылку. Разрешить POST здесь безопасно именно благодаря Idempotency-Key. Промежуточный слой также учитывает Retry-After на 429. Для списка хорошо работают Queue и восемь потоков даже в CRuby, потому что глобальная блокировка снимается во время ввода-вывода; в Rails оберните shorten в Active Job, чтобы спящий повтор не удерживал поток Puma.
Сокращение URL в PowerShell
В PowerShell сокращение URL - это один вызов Invoke-RestMethod, встроенного в Windows PowerShell 5.1 и PowerShell 7; он возвращает разобранный объект, а не строку JSON:
$body = @{ destination_url = 'https://example.com/spring-sale' } | ConvertTo-Json
$link = Invoke-RestMethod -Method Post `
-Uri 'https://api.elido.app/v1/links' `
-Headers @{ Authorization = "Bearer $env:ELIDO_API_KEY" } `
-ContentType 'application/json' `
-Body $body `
-TimeoutSec 10
$link.short_url
ConvertTo-Json не опционален. Передайте хеш-таблицу прямо в -Body - и PowerShell закодирует её как форму, а вы получите 400, похожую на проблему сервера. Для людей из других языков сюрприз идёт в обратную сторону: 401 или 404 здесь - завершающая ошибка, а не возвращаемое значение, поэтому вы перехватываете исключение вместо проверки кода состояния. PowerShell 7 добавляет -SkipHttpErrorCheck, который вместо исключения возвращает ответ.
Ключ в хранилище и целая колонка CSV
Ключ, набранный в файле .ps1, попадает в систему контроля версий, в Get-History и в любой журнал сеансов, включённый в организации. Модуль SecretManagement держит его в хранилище, поэтому ротация ключа - это обновление одной записи, а не поиск по скриптам на четырёх серверах. Типичная настоящая задача - колонка таблицы, а не один URL:
$env:ELIDO_API_KEY = Get-Secret -Name ElidoApiKey -AsPlainText
function Get-ShortLink([string] $Destination) {
$sha = [System.Security.Cryptography.SHA256]::Create()
$key = [BitConverter]::ToString(
$sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($Destination))).Replace('-', '')
$body = @{ destination_url = $Destination } | ConvertTo-Json
for ($attempt = 0; $attempt -lt 3; $attempt++) {
try {
return (Invoke-RestMethod -Method Post -Uri 'https://api.elido.app/v1/links' `
-Headers @{ Authorization = "Bearer $env:ELIDO_API_KEY"; 'Idempotency-Key' = $key } `
-ContentType 'application/json' -Body $body -TimeoutSec 10).short_url
}
catch {
$status = [int]$_.Exception.Response.StatusCode
if ($status -ne 429 -and $status -lt 500) { throw } # 400, 401, 403: give up
Start-Sleep -Seconds ([math]::Pow(2, $attempt))
}
}
throw "gave up after 3 attempts: $Destination"
}
$funcDef = ${function:Get-ShortLink}.ToString()
Import-Csv .\campaign-urls.csv | ForEach-Object -ThrottleLimit 8 -Parallel {
${function:Get-ShortLink} = $using:funcDef # ship the function into the runspace
$row = $_
$short = try { Get-ShortLink $row.destination } catch { "ERROR: $($_.Exception.Message)" }
[pscustomobject]@{ id = $row.id; destination = $row.destination; short_url = $short }
} | Export-Csv .\campaign-urls-short.csv -NoTypeInformation
-ThrottleLimit 8 держит восемь запросов в полёте, будь в файле пятьдесят строк или пятьдесят тысяч. Каждая итерация -Parallel выполняется в собственном runspace, поэтому функция передаётся через $using:, а вся конструкция требует PowerShell 7; на 5.1 честный ответ для нескольких сотен строк - обычный foreach. Ошибки попадают в выходную колонку, а поскольку ключ идемпотентности происходит из адреса назначения, повторный запуск файла после исправления трёх строк не создаёт дубликатов. Прежде чем что-то уйдёт в печать, выборочно проверьте несколько строк через curl.exe -sI. CSV уже лежит в Google Sheets? Бескодовый массовый импорт делает ту же работу.
Что SDK умеют сегодня
Официальные клиентские библиотеки существуют для четырёх языков (TypeScript как @elido/sdk, Python, Go, Ruby) и следуют спецификации OpenAPI. Пока опубликован только Go SDK как модуль github.com/elidoapp/elido-go (v0.1.0). SDK для TypeScript, Python и Ruby ещё не опубликованы в npm, PyPI или RubyGems. Поэтому каждый пример выше использует собственный HTTP-клиент своего языка, что и поддерживается для продакшен-интеграций сегодня. Когда библиотеки выйдут, страница API и SDK будет содержать инструкции по установке.
Когда они появятся, ожидайте, что они будут оборачивать маршруты в пределах рабочего пространства, так что ID пространства и domain_id вы будете передавать явно, и что они будут отличаться поведением повторов. Клиент TypeScript добавляет Idempotency-Key к каждому POST, PUT или PATCH и повторяет 502, 503 и 504 с паузами, оставляя 429 вам. Клиент Ruby тоже генерирует ключи. Он бросает типизированную ошибку лимита с retry_after, но сам никогда не повторяет. Клиенты Python и Go не повторяют вовсе. Какой бы библиотекой вы ни пользовались, политика повторов из раздела об ошибках остаётся в силе.
Для терминала, а не кодовой базы, руководство по CLI сокращателя ссылок оборачивает тот же вызов создания в curl и shell-функцию, а руководство по MCP-серверу подключает API к Claude, Cursor или любому другому MCP-клиенту.
Доступ к аналитике
Конечные точки аналитики предназначены только для чтения и находятся под /v1/workspaces/{id}/analytics/; руководство по API аналитики ссылок перечисляет каждый отчёт, его параметры и форму ответа. Самые частые запросы:
GET .../clicks/recent?from=…&to=…- отдельные клики, сначала новейшие, постранично черезnext_cursor. Полезно для конвейеров экспорта.GET .../timeseries?from=…&to=…&interval=day- сгруппированные по интервалам количества кликов;interval- этоhourилиday, аtzзадаёт часовой пояс интервалов.GET .../breakdown/country?from=…&to=…- распределение по географии.GET .../breakdown/referrer?from=…&to=…- распределение по источникам переходов.
Остальные отчёты - summary, links/top, прочие распределения (host, device, browser, destination) и топ-списки (top-countries, top-regions, top-cities, top-referrers, top-destinations). from и to - даты в формате YYYY-MM-DD, причём to не включается; добавьте link_id, чтобы сузить любой отчёт до одной ссылки, и limit, чтобы задать размер распределений и топ-списков.
Поток сырых кликов самый большой. Рабочее пространство с 10M кликов в месяц порождает около 600MB JSON сырых событий за месяц, так что вычитывайте его по расписанию, а не за один присест.
Вебхуки для событий ссылок
Вебхуки - это противоположность опросу: вместо того чтобы спрашивать API, что изменилось, вы получаете события ссылок и доменов на свою конечную точку. Настройте их на /dashboard/webhooks, указав URL конечной точки, нужные события (link.created, link.updated, link.expired и другие) и секрет подписи.
Событие click.created для отдельного клика есть в планах и пока недоступно, поэтому данные о кликах пока приходят из конечных точек аналитики. Каждая доставка содержит заголовок X-Elido-Signature (его также отправляют как X-Webhook-Signature) со значением v1=<hex>: это HMAC-SHA256 с секретом вашей конечной точки в качестве ключа над значением X-Webhook-Timestamp, точкой и сырым телом запроса. Проверяйте его перед обработкой, иначе любой вызывающий сможет отправить запрос на вашу конечную точку и выдать себя за Elido; руководство по проверке подписей вебхуков содержит эту проверку на нескольких языках.
Доставка - как минимум однократная. Неудачные доставки получают три попытки в сумме с интервалом в минуты. Сравнение вебхуков и опроса взвешивает два подхода к интеграции.
Рабочий пример: автоматизация кампании
Интеграция, которая мотивирует большинство обращений к API, выглядит так. Ваша маркетинговая автоматизация создаёт кампанию в Customer.io или HubSpot, и при её публикации срабатывает хук. Ваш обработчик создаёт короткую ссылку. Затем сохраняет её в кампании и возвращает для шаблона письма. В Node:
export async function onCampaignPublished(campaign: Campaign) {
const res = await fetch("https://api.elido.app/v1/links", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ELIDO_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `campaign-${campaign.id}-link`,
},
body: JSON.stringify({
destination_url: campaign.destinationUrl, // UTM parameters already in here
tags: [
"campaign",
`campaign:${campaign.id}`,
`batch:${campaign.batchId}`,
campaign.channel,
],
}),
signal: AbortSignal.timeout(10_000),
});
if (!res.ok)
throw new Error(`shorten failed: ${res.status} ${await res.text()}`);
const link = await res.json();
await campaignStore.update(campaign.id, {
linkId: link.id,
shortUrl: link.short_url,
});
return link;
}
Ключ идемпотентности происходит из ID кампании. Когда хук публикации срабатывает дважды (так и будет, ведь доставки как минимум однократные), второй вызов возвращает ту же ссылку вместо дубликата. Теги campaign: и batch: - ваши собственные ключи для связывания. Отчёты о кликах привязываются к кампании именно через них. О сквозной атрибуции с шаблонами UTM и передачей конверсий подробно рассказывает руководство по UTM-отслеживанию на примере всего конвейера.
Чего в API ещё нет
Кое-чего, о чём спрашивают команды, сегодня нет:
- Аналитика одной ссылки одним вызовом. Единого запроса, возвращающего все распределения по одной ссылке, не существует; сегодня клики, страны, источники, устройства и временной ряд запрашивают отдельно, поэтому выполняйте их параллельно в собственном коде.
- Повторная отправка вебхуков через API. Дашборд показывает историю доставок и умеет повторять их; API пока нет.
- Переименование слагов с 301. То же относится к полям
utmиmetadataпри создании; пока их нет, кладите UTM-параметры в адрес назначения, а ключи для связывания - в теги.
Моё практическое правило: если что-то есть в спецификации OpenAPI, стройте на этом. Если это только в блоге, включая эту статью, считайте запланированным.
Читайте также в блоге
Этот хаб стоит в кластере функций рядом со статьёй умные ссылки: как это работает, где описано, как движок редиректов разрешает ссылку на периферии.
- API сокращателя ссылок: лимиты запросов, повторы и идемпотентность - укрепление интеграции под продакшен-трафик.
- Сокращатели ссылок для разработчиков - что проверить в API, прежде чем строить на нём.
- Серверное отслеживание конверсий через короткие ссылки - расширение API до передачи конверсий.
- Как сократить URL без кода - пути через дашборд, телефон и таблицу.
- Поверхность продукта:
/features/api-sdksи/solutions/developers.
Частые вопросы
Как сократить URL через API?
Отправьте аутентифицированный POST с длинным URL в JSON-теле и прочитайте короткую ссылку из ответа. В Elido это POST https://api.elido.app/v1/links с заголовком Authorization: Bearer и вашим API-ключом и телом {"destination_url": "..."}; ответ 201 содержит short_url. Все языки в этом руководстве лишь обёртки вокруг одного этого запроса.
Как сократить URL в Python?
Вызовите requests.post для конечной точки links, передав ключ в заголовке Authorization: Bearer, json={"destination_url": url} и timeout, затем raise_for_status() и прочитайте response.json()["short_url"]. Для тысяч URL перейдите на httpx.AsyncClient с asyncio.Semaphore, чтобы одновременно в полёте было лишь несколько запросов.
Как сократить URL в JavaScript или Node.js?
Вызовите fetch в Node 18 или новее с методом POST, заголовком Bearer и JSON-телом, проверьте res.ok перед чтением тела и возьмите short_url из разобранного JSON. Запускайте это на сервере или в serverless-маршруте, никогда в коде браузера: ключ, попавший на страницу, прочитает любой, кто откроет DevTools.
Нужен ли SDK, чтобы пользоваться API сокращателя ссылок?
Нет. Создание ссылки - это один POST с двумя заголовками, и его отправит стандартный HTTP-клиент любого языка: requests или httpx, fetch, net/http, java.net.http, HttpClient, curl, Net::HTTP и Invoke-RestMethod. SDK окупается, когда вы работаете со многими конечными точками и хотите типизированные модели, но именно простые HTTP-версии из этого руководства мы поддерживаем для продакшена сегодня.
Как не дать повторам создавать дубликаты коротких ссылок?
Отправляйте заголовок Idempotency-Key и держите его одинаковым во всех повторах одного логического запроса, например как хеш адреса назначения или ID вашего заказа. Elido хранит первый успешный ответ 24 часа для каждого рабочего пространства и возвращает его на повторённый ключ, поэтому повтор после потерянного ответа вернёт исходную ссылку, а не создаст вторую.
Сколько коротких ссылок в секунду можно создавать через API?
Elido ограничивает трафик API для каждого рабочего пространства по алгоритму token bucket: 10 запросов в секунду со всплеском 20 на Free, 30 со всплеском 60 на Pro и 100 со всплеском 200 на Business. Ответ 429 содержит Retry-After: 1. Для больших пакетов POST /v1/links/bulk принимает до 100 ссылок в одном запросе.
Попробуйте Elido
Вставьте URL - получите короткую ссылку
Без регистрации. Ссылка живёт 30 дней. Зарегистрируйтесь, чтобы оставить её навсегда.
Без регистрации. Зарегистрируйтесь бесплатно и получите 50 ссылок в месяц.