11分で読了機能

URL短縮API: Python、JS、GoでURLを短縮する

Python、JavaScript、Go、Java、C#、PHP、Ruby、PowerShellでAPI呼び出し1回でURLを短縮し、冪等なリトライとバルク上限を加えます。

Marius Voß
DevRel · edge infra

APIでURLを短縮するには、認証付きのPOSTを1回送ります。長いURLを destination_url としてJSONボディに入れ、Authorization: Bearer <your key> を付けて https://api.elido.app/v1/links に送り、201 Created のレスポンスから short_url を読み取ります。どの言語でも連携はこれだけです。以下の8言語の違いは、リクエストの書き方だけです。そして、最初に問題になる本番環境の細部だけです。

最初の呼び出しは5分で終わります。厄介なのはその後、本番トラフィックが来てリトライ処理が重複を作り、ダッシュボードが同じ宛先の /foo-1、/foo-2、/foo-3 で埋まり、誰かがチケットを切る頃です。このガイドは、そのチームに先に読んでほしかったURL短縮APIの解説です。認証、最初の呼び出し、4つのリンクエンドポイント、冪等キー、エラーコード、レート制限、そして同じ短縮リンクのリクエストを、各ランタイムが必要とする習慣とともに8言語で示します。

まだサービスを選んでいる段階ですか? 無料URL短縮API比較では、ベンダーごとのリクエスト形式と無料枠の上限を並べています。以下はすべてElidoのREST APIが対象です。

お使いの言語へ直接移動できます。

  • Python: 1本なら requests、数千本ならセマフォ付きの httpx。
  • JavaScriptとNode.js: fetch、キーをブラウザに置かない理由、Promise.all の代わりのワーカープール。
  • Go: タイムアウト付きで共有する1つの http.Client、バルク用の errgroup.SetLimit。
  • Java: JDKの HttpClient、Semaphore の背後で動く仮想スレッド。
  • C#と.NET: PostAsJsonAsync、続いて IHttpClientFactory の型付きクライアント。
  • PHP、Laravel、WordPress: curl、リトライ付きの Http::、公開時の wp_remote_post。
  • Ruby: Net::HTTP.post、続いてPOSTを本当にリトライするミドルウェア付きのFaraday。
  • PowerShell: Invoke-RestMethod、金庫に保管したキー、CSVの列まるごと。

前提: APIキーとベースURL

ダッシュボードにサインインし、/dashboard/api-keys を開いてAPIキーを作成します。キーは elido_ で始まり、表示されるのは1回だけです。各キーは1つのワークスペースに紐づくため、ワークスペースAで発行したキーでワークスペースBにリンクは作れません。APIはキーだけからどのワークスペースかを判断します。マシンユーザーのトークン(CIシステム、社内ツール、マシン間連携向け)は /dashboard/machine-users にあり、個人キーとは独立してローテーションされます。

どちらの種類も、エンドポイントごとのスコープではなく、あらかじめ決まったワークスペースロール(viewer、editor、admin)を持ちます。リンクを作るだけのCIジョブには editor を付けます。APIキーの権限のガイドに、各ロールが届く範囲を一覧にしています。Webhookの変更には admin が必要で、その理由も説明しています。

Elido のAPIキー画面。3つのキーのプレフィックス、editorとviewerのロール、最終使用日、24時間のリクエスト数が並んでいる

ベースURLは https://api.elido.app で、リンク関連のルートは /v1 の下にあります。リダイレクト用ドメイン(Freeは f.elido.me、有料プランは s.elido.me、Businessは b.elido.me)は別の面です。短縮リンクはそこで解決され、APIはリンクの作成と編集を担います。APIリファレンスの元になっているOpenAPI 3.1仕様が契約です。仕様にあるフィールドはサポート対象です。

新しいアカウントがつまずく点が1つあります。所有者がメールアドレスを確認していないキーによる書き込みは 403(email_not_verified)で返ります。読み取りは引き続き動きます。マシンユーザーのトークンは対象外です。アドレスを1度確認すればエラーは消えます。

最初の呼び出し: 短縮リンクを作成する

以下の各言語セクションが再現するリクエストを、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])で、最大6文字です。

キーは環境変数(ここでは全例で ELIDO_API_KEY)に入れ、ソースには決して書かないでください。短縮サービスのキーは支出を伴う認証情報で、持っている人は誰でもあなたの枠でリンクを作れます。Nodeのセクションで説明するとおり、JavaScriptでは特に重要です。

実際に使う4つのエンドポイント

APIのエンドポイントは4つより多いものの、ほとんどの連携はこの範囲に収まります。フラットな /v1/links ルートはキーからワークスペースを推定します。/v1/workspaces/{workspace_id}/links 以下の正規パスも同じ動作で、IDを明記する必要があります。

/v1/links リソースを中心にした4つの主要リンクエンドポイントのハブ・アンド・スポーク図。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)こそ、冒頭の重複リンク問題の始まりです。対処は冪等キーで、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 ヘッダーを受け付けます。自分で選ぶ不透明な文字列です。キーが初めて成功レスポンスとともに届くと、サーバーはステータスコードとボディを (workspace, key) の範囲で24時間保存します。同じキーと同じリクエストが繰り返されると、保存されたレスポンスが返るため、リトライで2本目が作られることはなく、すでにあるリンクが返ります。

うまく機能するかは3つの細部で決まります。

  • 保存されるのは成功レスポンスだけです。429 や 500 はキャッシュされないので、同じキーでの次のリトライは本当の2回目の試行になります。
  • サーバーはメソッド、パス、ボディのフィンガープリントを取ります。ボディを変えてキーを再利用すると 422 と idempotency_key_mismatch が返ります。これは呼び出し側のバグで、リトライするものではありません。
  • DELETE はキーを受け付けません。2回削除しても、もともと冪等だからです。

キー自体は安定したものから作ります。業務イベント(order-12345-link)があればそこから、バッチジョブなら宛先URLのSHA-256ハッシュから作ります。試行ごとのランダムUUIDでは目的が果たせません。URLごとに1つなら、バッチを再実行するまでは機能します。

at-least-onceのキャンペーンフックが同じ冪等キーを持つ作成呼び出しを2回発行し、24時間キャッシュが2回目を重複排除して、リンクがちょうど1本だけ作られるパイプライン。

落とし穴は24時間の窓です。止まっていたジョブの3日目のリトライは新しいリンクを作ります。数日にわたるバッチでは、最初に成功した作成の id を保存し、再発行の前に照会してください。キーはワークスペースごとでもあります。同じキーでも2つのワークスペースなら2本のリンクができます。複数ワークスペース対応のAPIとしては正しい動作ですが、キーがグローバルだと思っていたチームは今でも驚きます。

宛先のハッシュをキーにすると、知っておくべき副作用が1つあります。24時間以内に同じURLを指す2つのキャンペーンは、1本のリンクを共有します。2本欲しいなら、キーにキャンペーンIDを入れてください。

エラー処理とステータスコード

エラーは、人が読める error と、場合によっては機械が読める code を持つ小さなJSONオブジェクトで返ります。

{ "error": "destination_url is required" }

出会うステータスコードは次のとおりです。

  • 400 - 検証エラー。destination_url の欠落、不正な形式の expires_at、無効な redirect_status など。ペイロードを直し、リトライはしません。
  • 401 - キーがない、失効している、または打ち間違い。ヘッダーがキーの前に半角スペースを挟んだ Bearer になっているか確認してください。
  • 402 - プランのリンク上限を使い切りました。リトライしても解決せず、アップグレードか次の期間まで待つ必要があります。
  • 403 - キーのロールでは実行できない(viewer キーはリンクを作れない)、アカウントのメールが未確認、または宛先が安全性スキャンでブロックされた("code": "destination_blocked")。
  • 404 - リンクが存在しないか、このキーからは見えません。403ではなく404を返すのは、呼び出し側が自分のものでないリソースを探れないようにするためです。
  • 409 - そのドメインでスラッグがすでに使われています。
  • 422 - 異なるボディで再利用された冪等キー、または許されないオプションの組み合わせ。
  • 429 - レート制限に達しました。Retry-After だけ待ち、同じ冪等キーで再試行します。
  • 500、502、503、504 - サーバー側の障害か一時的な障害。バックオフして同じキーでリトライします。
APIのステータスコードを、バックオフ付きでリトライする列(429、500、502、503、504)と、プログラミングエラーや競合のためリトライしない列(400、401、403、404、409)に振り分ける判断図。

この振り分けがリトライ方針のすべてで、以下の各言語セクションも同じように実装しています。429 と 5xx はリトライし、それ以外は諦めます。同じペイロードで 400 をリトライしても答えは変わらず、401 を3回リトライしても、失敗が遅くなるだけです。レート制限とリトライのガイドでは、ジッターと、無差別なリトライループが1つの障害を2つにしてしまう理由をさらに掘り下げています。

レート制限とバルクエンドポイント

APIトラフィックは、ワークスペースごとにトークンバケットで計測されます。

プラン持続レートバースト
Free10 req/s20
Pro30 req/s60
Business100 req/s200

バケットが満杯なら、バースト分を一度に送れます。その後は持続レートに落ち着きます。各APIキーにはワークスペースのバケットとは別に独自のバケットもあるため、暴走した1つのスクリプトがワークスペース全体の枠を使い切ることはありません。

拒否されたリクエストには 429、Retry-After: 1 ヘッダー、そしてどのバケットが空になったか(workspace か api_key)を示す X-RateLimit-Scope ヘッダーが付きます。残り枠を示すヘッダーはないので、ペース配分の基準にはできません。現実的なのは、同時実行数に固定の上限を設けたうえで 429 をリトライする方法です。Proなら同時8リクエストから始めるのが無難で、Freeなら4が合います。1つ癖に引っかかりました。429 のボディはJSON風のテキストが text/plain で送られるため、JSONのコンテンツタイプしか解析しないクライアントでは、オブジェクトのはずが単なる文字列に見えます。

大きな作業ですか? POST /v1/links/bulk は {"links": [...]} を最大100件まで受け取り、制限上は1リクエストとして数えられます。一部が失敗しても 200 で返り、ok と failed の件数、そして各項目の status(ok か error)が付くので、すべての項目を読んでください。ここを飛ばさないでください。返るリンクには slug はあっても short_url がないため、自分のドメインとスラッグからURLを組み立てます。Googleスプレッドシートからの一括インポートの解説では、この方法でシート全体を移します。

自分の言語でURLを短縮する: 共通の形

ここから先の各セクションは、最初の呼び出しと同じリクエストを送り、同じ4つの習慣を守ります。キーは環境変数から読むこと、すべてのリクエストにタイムアウトを付けること、ボディを解析する前にステータスコードを確認すること、2回実行されうる処理には安定した Idempotency-Key を付けることです。言語ごとに説明し直すことはしません。変わるのは書き方です。また、各ランタイムには、人がよく間違える点が1つずつあります。

どのセクションも、最小限で動く呼び出しから始まります。手で実行するスクリプトなら、それで十分です。続いて、そのランタイムの利用者が最もよく尋ねる本番向けの要素を扱います。Python、Go、Javaでは並行処理、C#ではクライアントの寿命、PHPではホスティング、Rubyではミドルウェア、PowerShellではシークレットとCSVです。書いたままの形で動かしたいですか? 無料プランでキーを作り、ELIDO_API_KEY としてエクスポートすれば、どのスニペットもそのまま動きます。

PythonでURLを短縮する

Pythonでは、URLの短縮は requests.post 1回です。キーは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をキーにして返ります。部分的な失敗も見えたままです。キーが宛先のハッシュなので、3行の不良行を直してリスト全体を再実行しても、何も二重には作られません。セマフォの内側でスリープするのは意図的です。待機を指示されたワーカーが、自分の枠を新しいリクエストに渡すべきではないからです。

JavaScriptとNode.jsでURLを短縮する

JavaScriptでは、URLの短縮は fetch によるPOST 1回です。Nodeはバージョン18から fetch をグローバルで提供しているので、何もインストールする必要はありません。

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 の解決済みPromiseとして届きます。Axiosの利用者もこれで一度は痛い目に遭います。たいていは、リンクがあるはずの場所に undefined が出る形で表れます。

キーはサーバーに置く

このスニペットをReactコンポーネントに貼ると、2つのことが壊れます。まずCORSがレスポンスをブロックします。キー認証のAPIは、任意のサイトに対して Access-Control-Allow-Origin を返さないからです。それ以上に大きな問題はエラーの裏にあります。フロントエンドのコードにあるキーはDevToolsを開けば誰でも読めますし、NEXT_PUBLIC_ というプレフィックスはそれを宣伝しているようなものです。そのため、ブラウザは単なるURLを自分のルートにPOSTし、キーはルートが付け足します。

ブラウザからのfetch呼び出しが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;
}

エッジランタイム(Vercel Edge、Cloudflare Workers、Deno)では node:crypto がないことがあります。その場合は crypto.subtle.digest("SHA-256", …) でハッシュを取れば、残りはそのままです。

GoでURLを短縮する

Goでは、URLの短縮に標準ライブラリの net/http と encoding/json を使います。ボディをマーシャルし、ヘッダーを2つ設定し、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 にはタイムアウトがまったくありません。まったくです。応答しないホストがあると、goroutineを何分も抱え込むことがあります。パッケージレベルのクライアントを1つ持てば呼び出しをまたいで接続プールが保たれ、NewRequestWithContext のおかげで、キャンセルされたCLIや切断された呼び出し元がリクエストを放置せず停止させられます。バッチが想定よりずっと多くのソケットを開くなら、トランスポートの MaxIdleConnsPerHost を上げてください。既定値の2はスクリプトでは見えませんが、8つのgoroutineが1つのホストを共有するとコストになります。

errgroup.SetLimitで上限付きファンアウト

URLごとに1つのgoroutineを起こすのはGoには安上がりでも、レート制限には致命的です。5,000件のURLが一度に5,000リクエストになります。errgroup の SetLimit なら、上限付きのファンアウトになります。

URLごとに1つの`goroutine`がレート制限を受けるリクエストで短縮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 は最初のエラーですべてをキャンセルします。これは全か無かのビルド工程には合いますが、できる限りのURLを短縮し、不良行を飛ばして進み、最後に失敗を報告するインポーターにはまったく不向きです。Go 1.22以降はループ変数が反復ごとになったため、古い u := u の行は不要になりましたが、ミューテックスは必須です。保護されていないマップは go test -race が指摘します。shorten の内部に 429 と 5xx のリトライを加えるときは、ctx.Done() と time.After に対する select を使い、バックオフを中断可能に保ってください。

JavaでURLを短縮する

Javaでは、URLの短縮は java.net.http.HttpClient によるPOST 1回です。Java 11からJDKに入っているので、最初の版に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"
}

2つのタイムアウトは別々の役割を果たします。connectTimeout はTCPとTLSのハンドシェイクを制限し、リクエストの timeout はやり取り全体を制限します。後者がないと、接続を受け付けたあと黙り込むサーバーがスレッドを抱え込みます。send が例外を投げるのは転送の失敗とタイムアウトだけで、HTTPエラーでは決して投げません。ステータスの確認が防御的プログラミングではなく契約である理由はそこにあります。HttpClient はアプリケーションごとに1つだけ作ってください。スレッドセーフで接続プールを持っており、呼び出しごとに作るとそのたびにTLSハンドシェイクをやり直すことになります。

セマフォの背後で動く仮想スレッド

仮想スレッドはJava 21で正式になりました。URLごとに1スレッドを用意しても現実的なコストで済みます。ただしレート制限は変わらないため、実際の仕事をするのは 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 の山を回収することになります。

C#と.NETでURLを短縮する

C#では、URLの短縮は、Bearerヘッダーをすでに持つ HttpClient に対する PostAsJsonAsync の呼び出し1回です。

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だらけのレコードを返し、失敗は3層先で NullReferenceException として表面化します。匿名オブジェクトは命名ポリシーなしでスネークケースのフィールド名を保ちます。実際のプロジェクトでは、シリアライザーのオプションに 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 はステータスコードしか残さないので、午前3時に 400 がどのフィールドに異議を唱えたのか推測するはめになります。バルク処理では、MaxDegreeOfParallelism = 8 を指定した Parallel.ForEachAsync が飛行中のリクエストに上限をかけ、手作りのセマフォより読みやすくなります。キーはローカルではユーザーシークレットに、本番では環境変数かボルトに置き、コミットされる appsettings.json には決して書かないでください。

PHP、Laravel、WordPressでURLを短縮する

PHPでは、URLの短縮にPHP標準添付のcurl拡張を使えます。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'];

2行は、誰もが一度は踏むバグのために存在します。CURLOPT_RETURNTRANSFER がないと、curlはJSONを出力に表示して true を返します。「レスポンスが空になる」という質問の大半の正体はこれです。また、CURLOPT_POSTFIELDS は配列を渡した途端にマルチパートのフォームエンコードに切り替わるので、json_encode こそがJSONリクエストにするものです。いまだに多くのPHPチュートリアルがGoogleの urlshortener/v1 やbit.lyのv3エンドポイントを呼んでいますが、どちらもとうに終了しており、goo.glのリンクがどうなったかはGoogle URL短縮の代替の記事で解説しています。

同じ短縮エンドポイントへの3つのPHP経路。共有ホスティングの素のcurl、アプリケーション内のGuzzleまたはLaravel、WordPress内のwp_remote_postで、いずれもAPIキーは環境から読み込む

LaravelのHttpクライアントとリトライ

LaravelやSymfonyのアプリでは、LaravelのHTTPクライアント(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 が3回リトライされずに済みます。素のGuzzleはcurlと動作が異なり、既定では 4xx で ClientException、5xx で ServerException を投げるので、それらを捕捉するか http_errors をfalseにします。夜間のインポートでは、'concurrency' => 8 を指定したGuzzleの Pool が、PHPにおける上限付き並列処理の答えです。PHP特有の落とし穴が1つあります。Webリクエストの内側の 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 というキーは安定しています。フックが2回発火しても、2回目の呼び出しは既存のリンクを受け取ります。フックをそもそも保守したくないなら、WordPressのURL短縮ガイドで、これをプラグインやノーコードの道と比較しています。

RubyでURLを短縮する

Rubyでは、URLの短縮は標準ライブラリの Net::HTTP.post 1回なので、スクリプトに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[] ではなく ENV.fetch を使うと、キーがないときにその場で失敗し、Bearer を送って分かりにくい 401 が返ってくるのを防げます。is_a?(Net::HTTPSuccess) の確認は、人が飛ばしがちな行です。Net::HTTPが例外を投げるのは、ソケットが死んだときとタイムアウトのときだけで、それ以外では投げません。また Net::HTTP.post はタイムアウト引数を受け取らないため、無人で動かすものなら open_timeout と read_timeout を指定できる Net::HTTP.start に切り替えてください。

リトライミドルウェア付きのFaraday

Railsアプリの中では、Faraday が、認証ヘッダー付きの接続オブジェクト、双方向のJSON、宣言的なリトライ方針を1つにまとめてくれます。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 があるからです。このミドルウェアは 429 の Retry-After も尊重します。リストを処理するなら、Queue と8スレッドの組み合わせがCRubyでもうまく働きます。グローバルロックはIO中に解放されるからです。Railsでは shorten をActive Jobで包み、スリープ中のリトライがPumaのスレッドを占有しないようにしてください。

PowerShellでURLを短縮する

PowerShellでは、URLの短縮は Invoke-RestMethod 1回で、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モジュールならキーをボルトに保管できるため、ローテーションは4台のサーバーのスクリプトを探し回る代わりに、1つのエントリを更新するだけで済みます。実際によくある作業は、1つの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 により、ファイルが50行でも5万行でも、飛行中のリクエストは8本に保たれます。-Parallel の各反復は独自のランスペースで動くため、関数は $using: で持ち込まれます。この構文全体にはPowerShell 7が必要で、5.1では数百行までなら素直な foreach が正解です。エラーは出力列に入り、冪等キーが宛先から作られるので、3行を直してファイルを再実行しても重複は生まれません。何かを印刷する前に、curl.exe -sI で数行を抜き取り確認してください。CSVがすでにGoogleスプレッドシートにあるなら、ノーコードの一括インポートが同じ仕事をします。

現在のSDKの状況

公式のクライアントライブラリは4言語(@elido/sdk としてのTypeScript、Python、Go、Ruby)にあり、OpenAPI仕様に沿っています。ただし、公開されているのはGo SDKだけで、モジュール github.com/elidoapp/elido-go(v0.1.0)として提供されています。TypeScript、Python、RubyのSDKは、npm、PyPI、RubyGemsにまだ公開されていません。そのため、上の例はすべて各言語自前のHTTPクライアントを使っており、これは現在、本番連携向けにサポートしている方法でもあります。ほかのライブラリが公開されたら、APIとSDKのページにインストール手順を掲載します。

公開後は、ワークスペース単位のルートをラップする形になるでしょう。その場合はワークスペースIDと domain_id を明示的に渡し、リトライの挙動は言語ごとに異なります。TypeScriptクライアントはPOST、PUT、PATCHのたびに Idempotency-Key を付け、502、503、504 をバックオフ付きでリトライし、429 は利用者に任せます。Rubyクライアントもキーを生成します。retry_after を持つ型付きのレート制限エラーを発生させますが、自分ではリトライしません。PythonとGoのクライアントはまったくリトライしません。どれを使っても、エラーのセクションのリトライ方針は引き続き当てはまります。

コードベースではなくターミナルから使うなら、URL短縮CLIガイドで同じ作成呼び出しをcurlとシェル関数に包んでいます。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 を付ければどのレポートも1本のリンクに絞れ、limit で内訳や上位リストの件数を決められます。

生のクリックフィードが最大です。月1,000万クリックのワークスペースでは、生イベントのJSONが月に約600MBになるので、一度に取り込まず、スケジュールを組んで順にページングしてください。

リンクイベントのWebhook

Webhookはポーリングの逆です。APIに何が変わったかを尋ねる代わりに、リンクとドメインのイベントが自分のエンドポイントに届きます。/dashboard/webhooks で、エンドポイントURL、必要なイベント(link.created、link.updated、link.expired など)、署名シークレットを指定して設定します。

クリックごとの click.created イベントはロードマップにあり、まだ利用できないため、現時点でクリックのデータは分析エンドポイントから取得します。各配信には X-Elido-Signature ヘッダー(X-Webhook-Signature としても送られます)が付き、値は v1=<hex> です。これは、エンドポイントのシークレットを鍵にして、X-Webhook-Timestamp の値、ドット、生のリクエストボディに対して計算したHMAC-SHA256です。処理の前に必ず検証してください。さもないと、誰でもあなたのエンドポイントにPOSTしてElidoになりすませます。Webhook署名の検証のガイドに、複数の言語での検証例があります。

配信はat-least-onceです。失敗した配信は、数分おきに合計3回試行されます。Webhookとポーリングの比較で、2つの連携パターンを比べています。

実例: キャンペーンの自動化

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から作られます。公開フックが2回発火しても(配信はat-least-onceなので、必ず起きます)、2回目の呼び出しは重複ではなく同じリンクを受け取ります。campaign: と batch: のタグは自前の結合キーで、クリックのレポートはこれを通じてキャンペーンに結び付きます。UTMテンプレートとコンバージョン転送を含むエンドツーエンドの計測については、UTM計測ガイドでパイプライン全体を順に説明しています。

APIにまだないもの

チームからよく聞かれるものの、今日はまだ使えないものがあります。

  • 1回の呼び出しでのリンク分析。 1本のリンクのすべての内訳を返す単一のリクエストはありません。現状ではクリック、国、リファラー、デバイス、時系列を別々にリクエストするので、自分のコードから並列に実行してください。
  • APIからのWebhook再送。 ダッシュボードでは配信履歴を確認して再送できますが、APIではまだできません。
  • 301付きのスラッグ名変更。 作成時の utm と metadata フィールドも同様です。それらができるまでは、UTMパラメータは宛先に、結合キーはタグに入れてください。

経験則はこうです。OpenAPI仕様にあるなら、それを前提に作ってください。ブログ記事にしかない場合は、この記事も含めて、計画中のものとして扱ってください。

ブログの関連記事

このハブは機能クラスターにあり、エッジでリダイレクトエンジンがリンクを解決する仕組みを扱うスマートリンクの解説の隣に位置します。

よくある質問

APIでURLを短縮するにはどうすればよいですか?

長いURLをJSONボディに入れた認証付きPOSTを送り、レスポンスから短縮リンクを読み取ります。Elidoでは、POST https://api.elido.app/v1/links に、Authorization: Bearer とAPIキーを続けたヘッダーと、ボディ {"destination_url": "..."} を付けて送ります。201レスポンスに short_url が含まれます。このガイドの各言語は、この1つのリクエストを包んだものです。

PythonでURLを短縮するにはどうすればよいですか?

リンクのエンドポイントに対して requests.post を使い、キーは Authorization: Bearer ヘッダーに入れ、json={"destination_url": url} とタイムアウトを指定します。raise_for_status() を呼んでから response.json()["short_url"] を読みます。数千件のURLなら、httpx.AsyncClient と asyncio.Semaphore に切り替え、同時に飛ぶリクエストを少数に抑えます。

JavaScriptやNode.jsでURLを短縮するにはどうすればよいですか?

Node 18以降で fetch をPOSTメソッド、Bearerヘッダー、JSONボディで呼び、ボディを読む前に res.ok を確認し、解析したJSONから short_url を取り出します。実行はサーバーかサーバーレスのルートで行い、ブラウザのコードでは行わないでください。ページに載せたキーはDevToolsを開けば誰でも読めます。

URL短縮APIを使うのにSDKは必要ですか?

いいえ。リンクの作成はヘッダー2つ付きのPOST 1回で、どの言語の標準HTTPクライアントでも送れます。requests や httpx、fetch、net/http、java.net.http、HttpClient、curl、Net::HTTP、Invoke-RestMethod などです。多数のエンドポイントを使い、型付きモデルが欲しい場合にSDKは役立ちますが、現在本番向けにサポートしているのは、このガイドの素のHTTP版です。

リトライで短縮リンクが重複して作られるのを防ぐには?

Idempotency-Key ヘッダーを送り、1つの論理的なリクエストのリトライごとに同じ値を保ちます。たとえば宛先のハッシュや注文IDです。Elidoはワークスペースごとに最初の成功レスポンスを24時間保存し、同じキーが繰り返されると再生します。そのため、レスポンスが失われた後のリトライでも、2本目を作らず元のリンクが返ります。

APIで1秒あたり何本の短縮リンクを作成できますか?

Elidoはワークスペース単位でAPIトラフィックをトークンバケットで制限しています。Freeは毎秒10リクエスト(バースト20)、Proは30(バースト60)、Businessは100(バースト200)です。429レスポンスには Retry-After: 1 が付きます。大きなバッチには、1回のリクエストで最大100本まで受け付ける POST /v1/links/bulk を使います。

Elidoを試す

URLを貼り付けて短縮リンクを取得

登録不要。リンクは30日間有効。永久に保存するには登録してください。

登録不要。無料登録すると月 50 件のリンクを作成できます。

Elidoを試す

EUホスティングの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

続きを読む