2分で読了機能

URL短縮API: 5言語で学ぶ30分クイックスタート

TypeScript、Python、Go、Ruby、PHPで、ゼロから動く短縮リンク自動化を作る方法。認証、冪等性、エラーハンドリング、本番運用でのつまずきポイントまで。

Marius Voß
DevRel · edge infra
TypeScript、Python、Go、Ruby、PHPのコードパネルが中央のElido APIエンドポイントを指す、5言語クイックスタート図。

URL短縮APIは、典型的なエンジニアリングチームのバックログの中では比較的小さなインテグレーションのひとつです。エンドポイントは3つ、認証ヘッダーがひとつ、JSONペイロードがひとつ。ドキュメントページは「最初の呼び出しまで5分」と約束します。しかし本番トラフィックが流れ始めると、リトライロジックが重複リンクを生み出し、ダッシュボードは同じ遷移先の/foo-1、/foo-2、/foo-3といったバリエーションで埋まり、誰かがチケットを起票することになります。

この記事では、実際のインテグレーションを一通り解説します。認証、最初の呼び出し、ほとんどのユースケースをカバーする4つのエンドポイント、冪等性、エラーハンドリング、レート制限、そして5分クイックスタートでは省かれがちな本番運用のつまずきポイントです。コードサンプルはTypeScript、Python、Go、Ruby、PHPの5言語で、最初の3つは公式SDK(@elido/sdk、elido-python、github.com/elido/elido-go)経由、残る2つは素のHTTPクライアント経由です。

前提条件

ダッシュボードにサインインし、/dashboard/api-keysに移動して、APIキーを作成してください(elido_で始まります)。トークンはワークスペーススコープです。ワークスペースAで発行されたトークンは、ワークスペースBでリンクを作成できません。マシンユーザートークン(CIシステム、社内ツール、マシン間連携向け)は/dashboard/machine-users配下で作成し、個人用キーとは独立してローテーションします。どちらの種類も、エンドポイントごとのスコープではなく、あらかじめ決められたワークスペースロール(viewer、editor、admin)を持つため、リンク作成しか行わないCIジョブにはeditorを与えてください。APIキーの権限ガイドでは、各ロールでアクセスできる範囲と、Webhookの変更にadminが必要な理由を説明しています。

ベースURLはhttps://api.elido.app/v1です。リダイレクトドメイン(f.elido.me、s.elido.me、b.elido.me)はAPIサーフェスとは別物です。短縮リンクはリダイレクトドメインで解決され、APIはそれらの作成・変更・読み取りのためのものです。

OpenAPI仕様はhttps://elido.app/openapi.jsonで公開されており、OpenAPI 3.1に準拠しています。公式SDKはこの仕様から生成され、APIのリリースごとに再公開されます。OpenAPIに対応した任意の言語で、自分自身のクライアントを生成することもできます。

最初の呼び出し

遷移先URLから短縮リンクを作成します。TypeScriptならたった5行です。

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

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

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

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

Python:

from elido import Elido

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

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

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

Go:

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

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

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

fmt.Println(link.ShortURL)

Ruby(公式SDKはなし。net/httpを使用):

require "net/http"
require "json"

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

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

PHP(Guzzle):

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

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

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

5つとも同じ結果になります。レスポンスボディには短縮URL、正規のリンクID、ワークスペースID、作成日時が含まれます。上の例のabc123にあたるスラッグは、リクエストでslugを渡さない限りサーバー側で生成されます。スラッグの文字体系はbase62([0-9A-Za-z])で、デフォルトの長さは6文字です。

実際に使うことになる4つのエンドポイント

APIにはこれ以上のエンドポイントがありますが、ほとんどのインテグレーションはこの4つの範囲内に収まります。

/v1/linksリソースを中心とした4つのコアリンクエンドポイントのハブアンドスポーク図: POST作成、GET読み取り、PATCH更新、DELETE削除、それぞれのつまずきポイント付き。

リンクを作成する

POST /v1/linksは、遷移先URLに加えて次のオプションフィールドを受け付けます。

  • slug - 自分で選ぶスラッグ(ドメイン上で一意である必要があります)。
  • domain_id - カスタムドメインのリンク用。/v1/linksでは省略した場合にプランのデフォルト短縮ドメインが使われます。ワークスペーススコープの/v1/workspaces/{workspace_id}/linksでは必須です。
  • title - ダッシュボードに表示されるラベル。
  • tags - 整理用の自由形式の文字列の配列。
  • expires_at - これを過ぎるとリンクが410 Goneを返すようになるRFC 3339形式のタイムスタンプ。
  • redirect_status - 301、302(デフォルト)、または307。
  • password - 作成時にはまだ受け付けられません。直後にPATCHで設定すると、リダイレクトの前にパスワードページが表示されます。
  • utmとmetadata - 計画中です。現在はUTMパラメータをdestination_urlに直接入れ、自分専用の結合キーはtagsに保持してください。

カスタムスラッグは、本番運用でチームがつまずきがちなフィールドです。同じドメイン上の別のリンクがすでに使っているスラッグを渡すと、APIは409 Conflictを返します。カウンターを付け足す(my-slug-1、my-slug-2)単純なリトライハンドラーは、冒頭で説明した重複リンク問題を引き起こします。正しいリトライの挙動は、後述の冪等性のセクションで説明します。

リンクを読み取る

GET /v1/links/{id}は、short_urlとすべての設定を含む完全なリンクレコードを返します。クリック数はリンクレコードにはなく、後述の分析エンドポイントから取得します。リンクIDが正規の識別子です。スラッグは変更できますが、IDは変わりません。

GET /v1/links?host=…&tags=…&limit=…は、フィルター付きでワークスペース内のリンクを一覧表示します。ページネーションはカーソルベースです。レスポンス内のnext_cursorは不透明な値で、次のリクエストでcursorクエリパラメータとしてそのまま返してください。

リンクを更新する

PATCH /v1/links/{id}は、作成時と同じフィールドを受け付けます。最も一般的な更新は、遷移先URLの変更(QRコードを刷り直さずにキャンペーンをローテーションする際に便利です)、タグの変更、expires_atの延長です。新しいslugを送ることで、同じPATCHでスラッグを変更します。旧スラッグは直ちに解決しなくなります。旧スラッグからの301を保持期間中返す専用のリネームエンドポイントは計画中で、まだ実装されていません。

リンクを削除する

DELETE /v1/links/{id}はソフトデリートを行い、204 No Contentを返します。リンクはリダイレクトを停止し、一覧と読み取りの呼び出しから除外されます。ゴミ箱ビュー、復元エンドポイント、ハードデリートまでの90日間の猶予期間は計画中です。現在、削除したリンクを戻すAPI呼び出しはありません。

冪等性キー

POST、PATCH、DELETEといった状態を変更するすべてのリクエストは、Idempotency-Keyヘッダーを受け付けます。ヘッダーの値は最大255文字までの不透明な文字列で、サーバーは(workspace_id, idempotency_key)をキーとしてレスポンスボディとステータスコードを24時間保存し、同じキーが再度提示された場合は保存済みのレスポンスを返します。

公式SDKは、指定がない場合、冪等性キーを自動的に生成します。上書きすることもできます。

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

典型的なユースケースはリトライループです。上流の注文処理の一部としてジョブがリンクを作成する場合、注文IDから冪等性キーを生成してください。同じジョブのリトライは同じキーを持つため、冪等性キャッシュにヒットし、2つ目のリンクを生成する代わりに最初に作成されたリンクが返されます。

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

重要なつまずきポイントとして、冪等性キャッシュは永続ではなく24時間しか保持されません。詰まったジョブを3日目にリトライすると、新しいリンクが作成されてしまいます。インテグレーションが複数日にまたがるバッチで動く場合は、最初に成功した作成呼び出しで返されたリンクIDを保存し、再実行の前にそれを検索してください。

2つ目のつまずきポイントは、冪等性がワークスペース単位であることです。同じキーでも2つのワークスペースで使えば、2つのリンクが作成されます。これはマルチワークスペースAPIとして正しい仕様ですが、キーがグローバルに一意だと思い込んでいるチームには意外に映るかもしれません。

エラーハンドリング

APIは標準的なHTTPステータスコードに加えて、構造化されたエラーボディを返します。

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

最もよく目にするコードは次の通りです。

  • 400 invalid_request - ペイロードの検証エラー。messageフィールドに該当フィールドが列挙されます。リトライせず、ペイロードを修正してください。
  • 401 unauthorized - トークンが存在しない、または無効です。トークンをローテーションせずにリトライしないでください。
  • 403 forbidden - トークンのロールがそのアクションを許可していません(viewerキーはリンクを作成できません)。/dashboard/api-keysでキーのロールを確認してください。
  • 404 not_found - リソースが存在しない、またはトークンにアクセス権がありません(未認可の呼び出し元にリソースの存在を漏らさないよう、403ではなく404を返しています)。
  • 409 conflict - スラッグがすでに使用されている、または同時編集が検出された(古いバージョンに対するPATCH)場合です。再取得してから再試行してください。
  • 429 rate_limit_exceeded - retry_afterの値に従ってバックオフしてください。
  • 500 internal_server_error - サーバー側の不具合です。同じ冪等性キーで安全にリトライできます。
  • 502 bad_gateway、503 service_unavailable、504 gateway_timeout -一時的なインフラの問題です。バックオフしてリトライしてください。

公式SDKは、429、500、502、503、504に対してジッター付きの指数バックオフを実装しています。400、401、403、404、409に対してはリトライしません。これらはプログラミングエラーかビジネスロジック上の競合であり、一時的な障害ではないためです。独自のHTTPクライアントを使う場合も同じパターンに従ってください。400を同じペイロードでリトライしても、結果は変わりません。

APIのステータスコードを、バックオフ付きでリトライする列(429、500、502、503、504)と、プログラミングエラーや競合エラーとしてリトライしない列(400、401、403、404、409)に振り分ける決定図。

エラーボディ内のrequest_idは、サポートチケットに含めるべきフィールドです。このIDがあれば、監査ログ、アプリケーションログ、プラットフォームのメトリクスを通じて任意のリクエストを追跡できますが、これがなければ追跡できません。

レート制限

公開されているレート制限は、Proではワークスペースあたり毎秒100リクエスト、Businessでは毎秒500リクエスト、Enterpriseでは個別交渉制です。無料プランは毎秒10リクエストです。

レート制限の状態は、すべてのAPIレスポンスの3つのヘッダーで確認できます。

  • X-RateLimit-Limit - 現在の1秒あたりの上限。
  • X-RateLimit-Remaining - 現在の1秒間に残っているリクエスト数。
  • X-RateLimit-Reset - バケットがリセットされるUnixタイムスタンプ。

毎秒100の上限は、バースト容量200のトークンバケット方式で実装されています。つまり、バケットが満杯であれば一度に200リクエストを送ることができ、その後は持続的な毎秒100のレートに落ち着きます。ほとんどの短縮リンク作成ジョブはこのバーストの範囲内に余裕をもって収まりますが、過去のクリックイベントをページングで取得するような分析重視のインテグレーションは、Proプランの余裕がある方が有利です。

バルク操作については、POST /v1/links/bulkエンドポイントが1リクエストあたり最大100本のリンクを受け付け、レート制限上は1ユニットとしてカウントされます。一度に100本を超えるリンクを作成するジョブには、このエンドポイントが適しています。トークンバケットに対するペース配分、どのステータスコードをリトライすべきか、冪等性キーがどのようにリトライによる重複を防ぐかについて詳しくは、本番環境でのレート制限・リトライ・冪等性をご覧ください。

SDKが素のHTTPと違うところ

公式SDKには、すぐに元が取れる4つの機能があります。

  • リトライ可能なステータスコードに対するバックオフ付き自動リトライ。
  • 明示的に指定しない場合の冪等性キーの自動生成。
  • 型付きエラー。これにより、catchブロックでJSONをパースする代わりにcatch (err) { if (err instanceof ElidoRateLimitError) { … } }のように書けます。
  • ページネーションイテレーター。一覧系エンドポイントは、手動でカーソルを扱う代わりに非同期イテレーターやジェネレーターを公開します。

Go SDKはさらに、内部で使われているHTTPクライアントを公開しており、既存のトレーシング基盤に組み込みたい場合に便利です。リポジトリのAPI + SDK機能ページが全体の仕様をカバーしており、APIリファレンスは/docs/api-referenceで公開されています。

分析データへのアクセス

分析エンドポイントは読み取り専用で、/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万クリックのワークスペースでは、月あたり約600MBの生イベントJSONが生成されます。この規模でのエクスポートについては、分析エクスポートガイドがJSONエンベロープを経由せず分析用データウェアハウスから直接ストリーミングする一括エクスポートの仕組みを解説しています。

リンクイベント向けWebhook

Webhookはポーリングの逆です。APIに何が変わったかを尋ねる代わりに、APIがリンクやドメインのイベントをあなたのエンドポイントに配信します。設定は/dashboard/webhooksから行います。

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

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

配信のセマンティクスはat-least-once(少なくとも1回)です。失敗した配信は、デフォルトで合計3回、数分単位のバックオフでリトライされます。詳しい形式とリトライの挙動については、webhook対ポーリングの記事で2つのインテグレーションパターンを比較しています。

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

API導入の動機として最も多いのが、次のようなインテグレーションです。マーケティングオートメーションがCustomer.ioやHubSpotでキャンペーンを作成する。キャンペーンが公開されるとフックが発火する。あなたのハンドラーが短縮リンクを作成し、それをキャンペーンレコードに紐づけ、メールテンプレートに埋め込むためキャンペーン管理ツールに送り返す、という流れです。

TypeScriptでは次のようになります。

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

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

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

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

冪等性キーはキャンペーンIDから導出されます。キャンペーン公開フックが2回発火した場合(実際に起こります。webhookの配信はat-least-onceです)、2回目の呼び出しは重複を作成せずに同じリンクを返します。campaign:とbatch:タグに自分専用の結合キーを持たせることで、Elidoのクリックイベントをキャンペーンに紐づけて追跡できます。専用のmetadataフィールドは計画中です。utmフィールドが提供されるまでは、UTMパラメータをcampaign.destinationUrl自体に入れてください。

UTMテンプレートとコンバージョン転送を使ったエンドツーエンドのキャンペーン計測については、UTM計測のコーナーストーン記事がパイプライン全体を解説しています。

まだAPIにないもの

よく質問されるが現時点では利用できないものが2つあります。

  • すべての内訳を1回の呼び出しで返す、単一リンク向けの分析GET。 現在のモデルでは、クリック数、国、リファラー、デバイス、時系列それぞれに個別の呼び出しが必要です。この集約機能はロードマップに載っています。現時点では、自分のコードからリクエストを並列に実行してください。
  • APIからのWebhook再送。 ダッシュボードはWebhookの配信履歴を公開しており再送にも対応していますが、APIはまだ対応していません。これもロードマップに載っています。

ある機能がOpenAPI仕様に含まれていれば、それはサポートされています。この記事に書かれていても仕様に含まれていない場合は、保証されたものではなく計画中のものとして扱ってください。

関連記事

Elidoを試す

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

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

Free、登録不要 · 1日あたり2件

Elidoを試す

EUホスティングのURL短縮サービス。カスタムドメイン、詳細な分析、オープンAPI付き。無料プラン - クレジットカード不要。

タグ
url shortener api
bitly api alternative
link shortener api
rest api short link
url shortener sdk
openapi 3.1
idempotency keys

続きを読む