URL短縮APIは、一般的なエンジニアリングチームのバックログの中でも比較的小規模な統合タスクの一つです。3つのエンドポイント、認証ヘッダー、JSONペイロード。ドキュメントには「5分で最初の呼び出しが可能」と書かれています。しかし、いざ本番トラフィックが発生すると、リトライロジックによって重複リンクが作成され、ダッシュボードは同じ遷移先を持つ /foo-1、/foo-2、/foo-3 といったバリエーションで埋め尽くされ、誰かがチケットを起票することになります。
このポストでは、実際の統合プロセスについて解説します。認証、最初の呼び出し、ほとんどのユースケースをカバーする4つのエンドポイント、べき等性、エラーハンドリング、レート制限、そして「5分間のクイックスタート」では省略されがちな本番環境での注意点について説明します。コード例として TypeScript、Python、Go、Ruby、PHP を紹介します。最初の3つは公式SDK(@elido/sdk、elido-python、github.com/elido/elido-go)を使用し、後半の2つは標準のHTTPクライアントを使用します。
事前準備#
ダッシュボードにサインインし、/settings/api に移動してパーソナルアクセストークンを作成してください。トークンのスコープはワークスペース単位です。ワークスペースAで発行されたトークンを使用して、ワークスペースBにリンクを作成することはできません。サービスアカウントトークン(CIシステム、内部ツール、マシン間統合用)も、Proプラン以上であれば同じ画面で作成可能です。これらには明示的なスコープ(links:write、analytics:read、domains:write)があり、パーソナルトークンとは独立してローテーションできます。
ベースURLは https://api.elido.app/v1 です。リダイレクト用ドメイン(f.elido.me、s.elido.me、b.elido.me)はAPIのサーフェスとは分離されています。短縮リンクはリダイレクト用ドメインで解決されますが、APIはそれらの作成、変更、読み取りに使用されます。
OpenAPI 仕様書は https://api.elido.app/v1/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)は、リクエストで custom_slug を渡さない限り、サーバーによって生成されます。スラッグのアルファベットは base62 ([0-9A-Za-z]) で、デフォルトの長さは6文字です。
実際に使用する4つのエンドポイント#
APIには4つ以上のエンドポイントがありますが、ほとんどの統合はこのセットだけで完結します。
リンクの作成#
POST /v1/links は遷移先URLに加え、以下のオプションフィールドを受け取ります:
custom_slug— 任意のスラッグ(ワークスペース内で一意である必要があります)。domain_id— カスタムドメインリンク用。省略した場合はワークスペースのプライマリドメインが使用されます。tags— 整理用の自由形式の文字列配列。utm— リダイレクト時に遷移先に付与されるキャンペーンパラメータ。expires_at— リンクが 410 Gone を返すようになる ISO 8601 形式のタイムスタンプ。password— 設定されている場合、リダイレクト前にパスワード入力ページを表示します。metadata— リダイレクトには影響しない、任意の JSON オブジェクト。独自のジョインキー(join keys)として有用です。
カスタムスラッグは、本番環境でチームを悩ませるフィールドです。同じワークスペース内の別のリンクですでに使用されているスラッグを渡すと、APIは 409 Conflict を返します。単純にカウンターを付与するような(my-slug-1, my-slug-2)ナイーブなリトライハンドラーは、冒頭で述べた重複リンクの問題を引き起こします。正しいリトライ動作については、後述のべき等性のセクションで説明します。
リンクの読み取り#
GET /v1/links/{id} は、現在のクリック数、最新のクリック日時、およびすべての設定を含む完全なリンクレコードを返します。リンクIDは正規の識別子です。スラッグは変更可能ですが(Pro以上でスラッグの変更をサポート)、IDは不変です。
GET /v1/links?domain_id=…&tag=…&limit=… は、フィルタを指定してワークスペース内のリンクをリスト表示します。ページネーションはカーソルベースです。レスポンスに含まれる next_cursor は不透明な値であり、次の中断箇所から再開するための cursor クエリパラメータとして使用します。
リンクの更新#
PATCH /v1/links/{id} は、作成時と同じフィールドを受け付けます。最も一般的な更新は、遷移先URLの変更(QRコードを再印刷せずにキャンペーンをローテーションする場合に便利)、タグの変更、expires_at の延長などです。スラッグの更新は別の POST /v1/links/{id}/rename エンドポイントで行います。これは、設定可能な保持期間(デフォルト30日間)の間、古いスラッグから 301 リダイレクトを処理します。
リンクの削除#
DELETE /v1/links/{id} は論理削除を行います。リンクはその後90日間 410 Gone を返し、その後に物理削除されます。ダッシュボードのゴミ箱ビューには論理削除されたリンクが表示され、90日以内であればダッシュボードまたは POST /v1/links/{id}/restore を通じて復元可能です。
べき等性キー(Idempotency keys)#
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つ目のリンクが作成されるのではなく、最初に作成されたリンクが返されます。
重要な注意点:べき等性キャッシュの寿命は無限ではなく、24時間です。停止していたジョブを3日後にリトライすると、新しいリンクが作成されてしまいます。数日間にわたるバッチ処理を行う場合は、最初の作成に成功した際に返されたリンクIDを保存し、再発行前に確認するようにしてください。
もう一つの注意点:べき等性はワークスペース単位です。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— トークンに必要なスコープがありません。/settings/apiでトークンのスコープ一覧を確認してください。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 をリトライしても結果は変わりません。
エラーボディに含まれる request_id は、サポートチケットに含めるべきフィールドです。このIDがあれば、監査ログ、アプリケーションログ、プラットフォームメトリクスからリクエストを追跡できます。逆に、このIDがないと追跡は困難です。
レート制限(Rate limits)#
公開されているレート制限は、Proプランでは1ワークスペースあたり毎秒100リクエスト、Businessでは500、Enterpriseでは交渉による制限となっています。Freeプランは 10 req/s です。
レート制限の状態は、すべてのAPIレスポンスの3つのヘッダーで確認できます:
X-RateLimit-Limit— 現在の秒間制限。X-RateLimit-Remaining— 現在の秒内で残っているリクエスト数。X-RateLimit-Reset— バケットがリセットされる Unix タイムスタンプ。
100/s の制限はトークンバケットアルゴリズムで実装されており、バースト容量は200です。つまり、バケットが満杯であれば一度に200リクエストを発行でき、その後は持続的な 100/s のレートに落ち着きます。ほとんどの短縮リンク作成ジョブはバースト内に収まります。過去のクリックイベントをページネーションで取得するような分析重視の統合では、Proプラン以上の余裕が役立ちます。
Businessプラン以上での一括操作には、1リクエストで最大1000リンクまで受け付け、レート制限を1ユニットとしてカウントする POST /v1/links/bulk エンドポイントが適しています。一度に100件以上のリンクを作成するジョブでは、このエンドポイントを使用するのが正解です。
SDKが提供し、通常のHTTPクライアントにはない機能#
公式SDKは、導入のメリットがすぐに感じられる4つの機能を備えています:
- リトライ可能なステータスコードに対するバックオフ付きの自動リトライ。
- 明示的に指定されない場合のべき等性キーの自動生成。
- 型定義されたエラー。これにより、catchブロックでJSONをパースする代わりに
catch (err) { if (err instanceof ElidoRateLimitError) { … } }と記述できます。 - ページネーションイテレータ。リスト表示エンドポイントが非同期イテレータやジェネレータとして公開されるため、手動でのカーソル処理が不要になります。
Go SDKではさらに、計測用に基底のHTTPクライアントを公開しています。これは既存のトレーシング設定に組み込みたい場合に便利です。リポジトリの API + SDKs 機能ページ で全容を確認できます。APIリファレンスは /docs/api-reference で公開されています。
分析データへのアクセス#
分析エンドポイントは読み取り専用で、/v1/workspaces/{id}/analytics/ 配下にあります。一般的なクエリは以下の通りです:
GET .../links/{id}/clicks?from=…&to=…— ページネーション付きの生のクリックイベント。エクスポートパイプラインに有用です。GET .../timeseries?from=…&to=…&bucket=day— 指定期間のバケット化されたクリック数。GET .../breakdown/country?from=…&to=…— 国別の内訳。GET .../breakdown/referrer?from=…&to=…— リファラー別の内訳。
生のクリックイベントフィードはデータ量が非常に大きくなります。月間1000万クリックのワークスペースでは、生のイベントデータだけで月間約600MBのJSONが生成されます。この規模のエクスポートについては、JSONエンベロープをバイパスして分析ウェアハウスから直接ストリーミングする ClickHouse エクスポートガイド を参照してください。
クリックイベントのWebhooks#
Webhooksはポーリングの逆です。APIに新しいクリックがあるか尋ねるのではなく、API側からエンドポイントにデータを届けます。/settings/webhooks で設定します:
await elido.webhooks.create({
url: "https://your-app.example/webhooks/elido",
events: ["link.click", "link.created", "link.expired"],
secret: process.env.WEBHOOK_SIGNING_SECRET,
});
各配信には、共有シークレットを使用してリクエストボディを HMAC-SHA256 で署名した Elido-Signature ヘッダーが含まれます。処理前に必ず署名を検証してください。これがないと、誰でもあなたのWebhookエンドポイントに投稿し、Elidoを装うことができてしまいます。
配信セマンティクスは「少なくとも1回(at-least-once)」で、最大72時間の指数バックオフによるリトライが行われます。詳細なデータ形式やリトライ動作については、Webhooks vs ポーリング のポストで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}`, campaign.channel],
utm: {
source: campaign.channel,
medium: "email",
campaign: campaign.slug,
},
metadata: { campaign_id: campaign.id, batch: campaign.batchId },
},
{
idempotencyKey: `campaign-${campaign.id}-link`,
},
);
await campaignStore.update(campaign.id, { shortUrl: link.shortUrl });
return link;
}
べき等性キーはキャンペーンIDから派生させています。キャンペーン公開フックが2回起動しても(Webhookの配信は「少なくとも1回」なので起こり得ます)、2回目の呼び出しは重複を作成せずに同じリンクを返します。metadata フィールドには独自のジョインキーを保持できるため、タグをパースすることなく Elido のクリックイベントをキャンペーンに関連付けることができます。
UTMテンプレートとコンバージョン転送を含むエンドツーエンドのキャンペーンアトリビューションについては、UTMトラッキングの要点 で詳細なパイプラインを解説しています。
APIでまだ提供されていない機能#
よく要望されますが、現在利用できない機能が2つあります:
- 1回の呼び出しですべての内訳を返す単一リンク分析GET。現在のモデルでは、クリック、国、リファラー、デバイス、時系列に対して個別の呼び出しが必要です。集約機能はロードマップに含まれていますが、現在はSDKが単一のヘルパーメソッドでリクエストを並列化して処理しています。
- API経由でのWebhookの再送(replay)。ダッシュボードではWebhookの配信履歴の確認と再送が可能ですが、APIはまだ対応していません。これもロードマップに含まれています。
OpenAPI 仕様に含まれている機能はサポートされています。このポストにあっても仕様にない場合は、保証されたものではなく計画中の機能として扱ってください。
関連資料#
- スマートリンクの解説 — featuresクラスターの基礎知識。エッジでのリダイレクトエンジンの解決方法について。
- Webhooks vs クリック追跡のためのポーリング — どちらの統合パターンをいつ使用すべきか。
- 短縮リンクによるサーバーサイドコンバージョントラッキング — APIをコンバージョン転送フローに拡張する方法。
- Google スプレッドシートからのキャンペーン一括インポート — 一括(bulk)エンドポイントの実践例。
- 運用の手引き:MCPサーバーガイド。ElidoのAPIサーフェスを Claude、Cursor、その他のMCP対応クライアントに接続する方法。
- 製品紹介:
/features/api-sdksおよび/solutions/developers。