11 мин чтенияВозможности

API URL-шортенера: 30-минутный быстрый старт на пяти языках

От нуля до рабочей автоматизации коротких ссылок на TypeScript, Python, Go, Ruby и PHP - авторизация, идемпотентность, обработка ошибок и подводные камни продакшена.

Marius Voß
DevRel · edge infra
Диаграмма быстрого старта на пяти языках с панелями кода для TypeScript, Python, Go, Ruby и PHP, каждая из которых указывает на центральный endpoint API Elido

API URL-шортенера - одна из менее значительных интеграций в типичном бэклоге инженерной команды. Три endpoint, заголовок авторизации, JSON payload. Страница документации обещает первый вызов за пять минут. Затем приходит продакшен-трафик, логика повторов создает дублирующиеся ссылки, дашборд заполняется вариантами /foo-1, /foo-2, /foo-3 одного и того же назначения, и кто-то заводит тикет.

Этот пост разбирает реальную интеграцию. Авторизацию, первый вызов, четыре endpoint, которые покрывают большинство сценариев использования, идемпотентность, обработку ошибок, лимиты запросов и подводные камни продакшена, которые пятиминутный быстрый старт обходит стороной. Примеры кода на TypeScript, Python, Go, Ruby и PHP - первые три через официальные SDK (@elido/sdk, elido-python, github.com/elido/elido-go), последние два через обычные HTTP-клиенты.

Что нужно заранее

Войдите в дашборд, перейдите на /dashboard/api-keys и создайте API-ключ (он начинается с elido_). Токены привязаны к рабочему пространству - токен, выпущенный в рабочем пространстве A, не может создавать ссылки в рабочем пространстве B. Токены машинных пользователей (для CI-систем, внутренних инструментов, интеграций machine-to-machine) создаются в /dashboard/machine-users и ротируются независимо от личных ключей. Оба вида несут заранее заданную роль в рабочем пространстве (viewer, editor или admin), а не разрешения по каждому endpoint, так что выдавайте CI-заданию роль editor, если оно только создает ссылки. В руководстве по разрешениям API-ключей перечислено, к чему имеет доступ каждая роль, включая объяснение, почему для изменений webhook нужна роль admin.

Базовый URL - https://api.elido.app/v1. Домены редиректа (f.elido.me, s.elido.me, b.elido.me) отделены от поверхности API. Ваши короткие ссылки разрешаются на домене редиректа; API служит для их создания, изменения и чтения.

Спецификация OpenAPI опубликована по адресу https://elido.app/openapi.json и соответствует OpenAPI 3.1. Официальные SDK генерируются из этой спецификации и переиздаются при каждом релизе API; вы также можете сгенерировать собственный клиент на любом языке, поддерживающем OpenAPI.

Первый вызов

Создайте короткую ссылку из целевого URL. Пять строк на TypeScript:

import { Elido } from "@elido/sdk";

const elido = new Elido({ token: process.env.ELIDO_TOKEN! });

const link = await elido.links.create({
  destinationUrl: "https://shop.example.com/spring-sale",
});

console.log(link.shortUrl); // https://s.elido.me/abc123

Python:

from elido import Elido

client = Elido(token=os.environ["ELIDO_TOKEN"])

link = client.links.create(
    destination_url="https://shop.example.com/spring-sale",
)

print(link.short_url)  # https://s.elido.me/abc123

Go:

import "github.com/elido/elido-go/v2/elido"

client := elido.NewClient(elido.WithToken(os.Getenv("ELIDO_TOKEN")))

link, err := client.Links.Create(ctx, &elido.LinkCreateInput{
    DestinationURL: "https://shop.example.com/spring-sale",
})
if err != nil {
    return fmt.Errorf("create link: %w", err)
}

fmt.Println(link.ShortURL)

Ruby (официального SDK нет - используем net/http):

require "net/http"
require "json"

uri = URI("https://api.elido.app/v1/links")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{ENV['ELIDO_TOKEN']}"
req["Content-Type"] = "application/json"
req.body = { destination_url: "https://shop.example.com/spring-sale" }.to_json

res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
link = JSON.parse(res.body)
puts link["short_url"]

PHP (Guzzle):

$client = new GuzzleHttp\Client(['base_uri' => 'https://api.elido.app/v1/']);

$res = $client->post('links', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('ELIDO_TOKEN')],
    'json'    => ['destination_url' => 'https://shop.example.com/spring-sale'],
]);

$link = json_decode((string) $res->getBody(), true);
echo $link['short_url'];

Все пять вариантов дают одинаковый результат. Тело ответа содержит короткий URL, канонический ID ссылки, ID рабочего пространства и метку времени создания. Slug - abc123 в примере выше - генерируется сервером, если вы не передали slug в запросе. Алфавит slug - base62 ([0-9A-Za-z]); длина по умолчанию - шесть символов.

Четыре endpoint, которыми вы будете реально пользоваться

В API больше четырех endpoint, но большинство интеграций остаются в пределах этого набора.

Диаграмма-звезда четырех основных endpoint ссылок вокруг ресурса /v1/links: POST создание, GET чтение, PATCH обновление и DELETE удаление, каждый со своим ключевым подводным камнем.

Создание ссылки

POST /v1/links принимает целевой URL плюс необязательные поля:

  • slug - slug, который вы выбираете сами (должен быть уникален на домене).
  • domain_id - для ссылок на собственном домене; в /v1/links используется короткий домен по умолчанию вашего тарифа, если поле не указано. Для пути с областью рабочего пространства /v1/workspaces/{workspace_id}/links оно обязательно.
  • title - метка, отображаемая в дашборде.
  • tags - массив произвольных строк для организации.
  • expires_at - метка времени RFC 3339, после которой ссылка возвращает 410 Gone.
  • redirect_status - 301, 302 (по умолчанию) или 307.
  • password - пока не принимается при создании; сразу после этого задайте его через PATCH, и редирект будет сначала показывать страницу с паролем.
  • utm и metadata - запланированы. Сегодня добавляйте UTM-параметры прямо в destination_url, а собственные ключи связывания храните в tags.

Собственный slug - это поле, которое кусает команды в продакшене. Если вы передадите slug, уже занятый другой ссылкой на том же домене, API вернёт 409 Conflict. Наивный обработчик повторов, добавляющий счётчик (my-slug-1, my-slug-2), создаёт проблему дублирующихся ссылок, описанную во введении. Правильное поведение при повторе описано в разделе об идемпотентности ниже.

Чтение ссылки

GET /v1/links/{id} возвращает полную запись ссылки, включая short_url и всю конфигурацию. Количество кликов в записи ссылки отсутствует, оно приходит из расположенных ниже endpoint аналитики. ID ссылки - канонический идентификатор: slug может меняться, ID - нет.

GET /v1/links?host=…&tags=…&limit=… перечисляет ссылки в рабочем пространстве с фильтрами. Пагинация курсорная; next_cursor в ответе непрозрачен и передаётся обратно как параметр запроса cursor в следующем запросе.

Обновление ссылки

PATCH /v1/links/{id} принимает те же поля, что и создание. Самые частые обновления: изменение целевого URL (полезно для ротации кампании без перепечатки QR-кодов), изменение тегов, продление expires_at. Slug меняется через тот же PATCH, если передать новый slug. Старый slug сразу перестаёт разрешаться; отдельный endpoint переименования, сохраняющий 301 со старого slug на период хранения, запланирован, но ещё не реализован.

Удаление ссылки

DELETE /v1/links/{id} выполняет мягкое удаление и возвращает 204 No Content. Ссылка перестаёт перенаправлять и исчезает из ответов запросов списка и чтения. Раздел корзины с endpoint восстановления и 90-дневным окном до окончательного удаления запланирован; сегодня API-вызова для восстановления удалённой ссылки нет.

Ключи идемпотентности

Каждый изменяющий запрос - POST, PATCH, DELETE - принимает заголовок Idempotency-Key. Значение заголовка - непрозрачная строка длиной до 255 символов; сервер хранит тело ответа и код статуса в течение 24 часов с ключом (workspace_id, idempotency_key) и возвращает сохраненный ответ, если тот же ключ передан снова.

Официальные SDK генерируют ключи идемпотентности автоматически, если они не переданы. Вы можете переопределить их:

const link = await elido.links.create(
  { destinationUrl: "https://shop.example.com/spring-sale" },
  { idempotencyKey: "order-12345-link" },
);

Типичный сценарий использования - цикл повторов. Если ваше задание создает ссылку в рамках обработки входящего заказа, генерируйте ключ идемпотентности из ID заказа. Повтор того же задания увидит тот же ключ, попадет в кеш идемпотентности и вернет изначально созданную ссылку, а не создаст вторую.

Конвейер, в котором хук кампании с гарантией доставки не менее одного раза делает два вызова создания с одним и тем же ключом идемпотентности; кеш на 24 часа дедуплицирует второй вызов, так что создается ровно одна ссылка.

Ключевой подводный камень: кеш идемпотентности живет 24 часа, а не вечно. Повтор на третий день зависшего задания создаст новую ссылку. Если интеграция работает на многодневных пакетах, сохраняйте ID ссылки, возвращенный первым успешным созданием, и ищите его перед повторной отправкой.

Второй подводный камень: идемпотентность работает в рамках рабочего пространства. Один и тот же ключ в двух рабочих пространствах создаст две ссылки. Это правильная семантика для API с несколькими рабочими пространствами, но она может удивить команды, которые считают, что ключ глобально уникален.

Обработка ошибок

API возвращает стандартные коды статуса HTTP плюс структурированное тело ошибки:

{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Workspace rate limit of 100 req/s exceeded. Retry after 1 second.",
    "request_id": "req_01HXYZAB123",
    "retry_after": 1
  }
}

Коды, которые вы будете видеть чаще всего:

  • 400 invalid_request - ошибка валидации payload. Поле message перечисляет конкретные поля. Не повторяйте запрос; исправьте payload.
  • 401 unauthorized - токен отсутствует или недействителен. Не повторяйте запрос без ротации токена.
  • 403 forbidden - роль токена не разрешает действие (ключ уровня viewer не может создавать ссылки). Проверьте роль ключа на /dashboard/api-keys.
  • 404 not_found - ресурс не существует, либо у токена нет к нему доступа (мы возвращаем 404, а не 403, чтобы не раскрывать существование ресурса неавторизованным вызывающим).
  • 409 conflict - slug уже занят, либо обнаружено одновременное редактирование (PATCH к устаревшей версии). Запросите заново и повторите попытку.
  • 429 rate_limit_exceeded - снизьте частоту согласно значению retry_after.
  • 500 internal_server_error - ошибка на стороне сервера. Безопасно повторить с тем же ключом идемпотентности.
  • 502 bad_gateway, 503 service_unavailable, 504 gateway_timeout - временные проблемы инфраструктуры. Снизьте частоту и повторите попытку.

Официальные SDK реализуют экспоненциальную задержку с разбросом (jitter) для кодов 429, 500, 502, 503 и 504. Они не повторяют запросы при 400, 401, 403, 404 или 409 - это ошибки программирования или конфликты бизнес-логики, а не временные сбои. Пользовательские HTTP-клиенты должны следовать той же логике; повтор запроса с кодом 400 и тем же payload не даст другого результата.

Схема принятия решения, разделяющая коды статуса API на колонку «повторить с задержкой» (429, 500, 502, 503, 504) и колонку «не повторять» (400, 401, 403, 404, 409) для ошибок программирования и конфликтов.

request_id в теле ошибки - поле, которое нужно указывать в тикетах поддержки. Мы можем отследить любой запрос по этому ID через журнал аудита, лог приложения и метрики платформы - и не можем отследить запрос без него.

Лимиты запросов

Опубликованные лимиты - 100 запросов в секунду на рабочее пространство на тарифе Pro, 500 на Business и согласованный лимит на Enterprise. Бесплатный тариф - 10 запросов в секунду.

Состояние лимита отображается в трех заголовках ответа на каждый ответ API:

  • X-RateLimit-Limit - текущий лимит в секунду.
  • X-RateLimit-Remaining - количество запросов, оставшихся в текущей секунде.
  • X-RateLimit-Reset - метка времени Unix, когда сбрасывается бакет.

Лимит 100/с реализован как token bucket с емкостью всплеска 200 - это значит, что вы можете сразу отправить 200 запросов, если бакет полон, а затем перейти на устойчивую скорость 100/с. Большинство заданий по созданию коротких ссылок комфортно умещаются во всплеск; интеграциям с большим объемом аналитики, которые постранично проходят исторические события кликов, пригодится запас тарифа Pro.

Для массовых операций endpoint POST /v1/links/bulk принимает до 100 ссылок за запрос и засчитывается как одна единица лимита. Это правильный endpoint для любого задания, создающего больше сотни ссылок за раз. Более глубокий разбор темпа относительно token bucket, выбора кодов статуса для повтора и того, как ключи идемпотентности не дают повторам дублировать ссылки, - в статье лимиты запросов, повторы и идемпотентность в продакшене.

Что дают SDK сверх обычного HTTP

Официальные SDK поставляют четыре вещи, которые быстро окупаются:

  • Автоматический повтор с задержкой для кодов статуса, допускающих повтор.
  • Генерация ключей идемпотентности, если они не заданы явно.
  • Типизированные ошибки, так что можно написать catch (err) { if (err instanceof ElidoRateLimitError) { … } } вместо разбора JSON в блоках catch.
  • Итераторы пагинации, так что списочные endpoint отдают асинхронные итераторы или генераторы вместо ручной работы с курсором.

Go SDK дополнительно предоставляет доступ к базовому HTTP-клиенту для инструментирования - полезно, если вы хотите встроить его в вашу существующую систему трассировки. Страница функции API + SDK описывает полную поверхность; справочник API опубликован по адресу /docs/api-reference.

Доступ к аналитике

Endpoint аналитики доступны только для чтения и находятся в /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, чтобы задать размер разбивок и списков лидеров.

Поток сырых событий кликов - самый объемный. Рабочее пространство с 10 млн кликов в месяц выдает около 600 МБ JSON сырых данных о событиях в месяц. Для экспорта в таком масштабе руководство по экспорту аналитики описывает механизм массового экспорта, который обходит JSON-обертку и передает данные напрямую из аналитического хранилища.

Webhook для событий ссылок

Webhook - это противоположность опроса: вместо того чтобы вы спрашивали API, что изменилось, API сам доставляет события ссылок и доменов на ваш endpoint. Настраивается в /dashboard/webhooks:

await elido.webhooks.create({
  url: "https://your-app.example/webhooks/elido",
  events: ["link.created", "link.updated", "link.expired"],
  secret: process.env.WEBHOOK_SIGNING_SECRET,
});

Событие click.created на каждый клик пока в дорожной карте, но еще недоступно, так что сегодня данные о кликах приходят из endpoint аналитики. Каждая доставка включает заголовок X-Elido-Signature (также отправляется как X-Webhook-Signature) со значением v1=<hex>: HMAC-SHA256 с ключом вашего секрета endpoint, вычисленный по значению X-Webhook-Timestamp, точке и сырому телу запроса. Проверяйте подпись перед обработкой - без этого любой вызывающий может отправить запрос на ваш webhook-endpoint и выдать себя за Elido.

Семантика доставки - «не менее одного раза»: неудачная доставка повторяется с задержкой в несколько минут, всего по умолчанию три попытки. Подробную форму и поведение повторов сравнивает статья webhook против опроса, которая разбирает оба паттерна интеграции.

Разобранный пример: автоматизация кампаний

Интеграция, которая чаще всего мотивирует использование API, выглядит так. Ваша маркетинговая автоматизация создает кампанию в Customer.io или HubSpot. Хук срабатывает, когда кампания публикуется. Ваш обработчик создает короткую ссылку, привязывает ее к записи кампании и отправляет обратно в инструмент управления кампаниями для подстановки в шаблон письма.

На TypeScript:

import { Elido } from "@elido/sdk";

const elido = new Elido({ token: process.env.ELIDO_TOKEN! });

export async function onCampaignPublished(campaign: Campaign) {
  const link = await elido.links.create(
    {
      destinationUrl: campaign.destinationUrl,
      tags: [
        "campaign",
        `campaign:${campaign.id}`,
        `batch:${campaign.batchId}`,
        campaign.channel,
      ],
    },
    {
      idempotencyKey: `campaign-${campaign.id}-link`,
    },
  );

  await campaignStore.update(campaign.id, { shortUrl: link.shortUrl });
  return link;
}

Ключ идемпотентности выводится из ID кампании. Если хук публикации кампании срабатывает дважды (а так и бывает - доставка webhook гарантирует «не менее одного раза»), второй вызов вернет ту же ссылку, не создав дубликат. Теги campaign: и batch: содержат ваши собственные ключи связывания, чтобы можно было сопоставить события кликов Elido с кампанией; отдельное поле metadata для этого запланировано. UTM-параметры должны находиться в самой campaign.destinationUrl, пока поле utm не появится.

Для сквозной атрибуции кампаний с UTM-шаблонами и пересылкой конверсий полный конвейер разбирает опорная статья про отслеживание UTM.

Чего пока нет в API

Две вещи, о которых часто спрашивают, но которых пока нет:

  • Единый GET аналитики по одной ссылке, возвращающий все разбивки за один вызов. Текущая модель требует отдельных вызовов для кликов, страны, источника перехода, устройства и временного ряда. Агрегация пока в дорожной карте; сейчас выполняйте запросы параллельно из собственного кода.
  • Повтор доставки webhook из API. Дашборд показывает историю доставки webhook и поддерживает повтор; API пока нет. Это тоже в дорожной карте.

Если функция есть в спецификации OpenAPI, она поддерживается. Если она упомянута в этом посте, но не в спецификации, считайте ее запланированной, а не гарантированной.

Читайте также

Попробуйте Elido

Вставьте URL - получите короткую ссылку

Без регистрации. Ссылка живёт 30 дней. Зарегистрируйтесь, чтобы оставить её навсегда.

Бесплатно, без регистрации · 2 в день

Попробуйте Elido

URL-сокращатель с хостингом в ЕС: собственные домены, глубокая аналитика, открытый API. Бесплатный тариф - без банковской карты.

Теги
url shortener api
bitly api alternative
link shortener api
rest api short link
url shortener sdk
openapi 3.1
idempotency keys

Читать дальше