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

Читати далі