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

API сокращателя ссылок: URL в Python, JS и Go

Сократите URL одним вызовом API в Python, JavaScript, Go, Java, C#, PHP, Ruby или PowerShell, а затем добавьте идемпотентные повторы и пакеты.

Marius Voß
DevRel · edge infra

Чтобы сократить 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, и там объяснено почему.

Страница API-ключей Elido со списком из трёх ключей: префиксы, роли editor и viewer, даты последнего использования и число запросов за 24 часа

Базовый 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.

Диаграмма «ступица со спицами» с четырьмя основными конечными точками ссылок вокруг ресурса /v1/links: POST создаёт, GET читает, PATCH обновляет, DELETE удаляет, и у каждой её главный подводный камень.

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

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-часовой кэш убирает второй, поэтому создаётся ровно одна ссылка.

Подвох - в 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 - серверный или временный сбой. Сделайте паузу и повторите с тем же ключом.
Разделение кодов состояния API на колонку «повторять с паузой» (429, 500, 502, 503, 504) и колонку «не повторять» (400, 401, 403, 404, 409) для ошибок программирования и конфликтов.

Это разделение и есть вся политика повторов, и каждый языковой раздел ниже реализует её одинаково: повторять 429 и 5xx, на остальном сдаваться. Повтор 400 с тем же телом не даст другого ответа, а три повтора 401 - просто более медленный сбой. Руководство о лимитах запросов и повторах глубже разбирает джиттер и то, почему слепые циклы повторов превращают один сбой в два.

Лимиты запросов и пакетная конечная точка

Трафик API измеряется для каждого рабочего пространства по алгоритму token bucket:

ТарифПостоянная скоростьВсплеск
Free10 запросов/с20
Pro30 запросов/с60
Business100 запросов/с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. Ключ добавляет маршрут.

Вызов fetch из браузера к API сокращателя ссылок, заблокированный CORS, с ключом API в сборке, рядом с правильным путём, где браузер обращается к серверному маршруту, который хранит ключ и пересылает запрос

Проверяйте 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 даёт разветвление с потолком.

Одна горутина на URL заливает API сокращателя запросами, упирающимися в лимит, рядом с 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.

Три пути PHP к одной конечной точке сокращателя: простой curl на виртуальном хостинге, Guzzle или Laravel в приложении и wp_remote_post внутри WordPress, и все читают API-ключ из окружения

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, стройте на этом. Если это только в блоге, включая эту статью, считайте запланированным.

Читайте также в блоге

Этот хаб стоит в кластере функций рядом со статьёй умные ссылки: как это работает, где описано, как движок редиректов разрешает ссылку на периферии.

Частые вопросы

Как сократить 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 ссылок в месяц.

Попробуйте Elido

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

Теги
url shortener api
how to shorten a url in python
shorten url javascript node
golang url shortener api
shorten url java c# php
shorten url ruby powershell

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