mcpField
P
polar-integration

polar-integration

github.com/polarsource開発6242 回インストール1 日前に更新
リポジトリ

polar-integration について

`@polar-sh/sdk` で Polar を統合する、フレームワーク非依存のガイドです。開発者向けに、Web 標準を使ったチェックアウト、カスタマーポータル、Webhook の HTTP エンドポイント実装を扱います。

主な機能

  1. 01チェックアウトセッションの作成とリダイレクト
  2. 02カスタマーポータルセッションの作成
  3. 03Webhook 署名の検証
  4. 04フレームワーク別ハンドラーの調整

活用シーン

  1. 01アプリにホスト型チェックアウトを追加
  2. 02顧客によるサブスクリプションや請求書の管理
  3. 03Polar の Webhook イベントの受信と検証

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/polarsource/skills --skill polar-integration

インストール後、Claude Code で/polar-integrationを入力して読み込まれたか確認します。

詳細

Polar SDK の統合

@polar-sh/sdk を直接使って、Polar の主要な 3 つの HTTP エンドポイント(Checkout、Customer Portal、Webhooks)を接続するための単独ガイドです。各レシピはフレームワークに依存しない Web 標準(Request/Response)に基づいており、各セクションではフレームワークごとの簡単な慣用的な調整方法も説明します。

セットアップ

SDK をインストールします。

npm install @polar-sh/sdk
# or pnpm / yarn / bun

@polar-sh/nextjs、@polar-sh/express、@polar-sh/hono などの @polar-sh/<framework> パッケージはインストールしないでください。これらは非推奨です。これらのレシピに必要なのは @polar-sh/sdk だけです。

必須の環境変数(process.env、Deno.env、import.meta.env など、使用するフレームワークが提供するローダーを使ってください)。

  • POLAR_ACCESS_TOKEN — Polar ダッシュボードで発行した組織のアクセストークンです。
  • POLAR_WEBHOOK_SECRET — Webhook のレシピでのみ必要です。
  • POLAR_SERVER — 開発中は sandbox に設定し、本番環境では省略する(または production に設定する)ことをおすすめします。

クライアントを 1 つ作成し、再利用します。

import { Polar } from "@polar-sh/sdk";

const polar = new Polar({
  accessToken: process.env.POLAR_ACCESS_TOKEN,
  server: (process.env.POLAR_SERVER as "sandbox" | "production") ?? "production",
});

以下の 3 つのレシピはそれぞれ独立しています。ユーザーから依頼されたものだけを実装してください。


レシピ 1 — Checkout エンドポイント

目的: Polar のチェックアウトセッションを作成し、ブラウザーをホストされたチェックアウトページに 302 リダイレクトする HTTP エンドポイントです。

基盤となる API: POST /v1/checkouts/(SDK:polar.checkouts.create(...))。スキーマの全文は https://polar.sh/docs/api-reference/checkouts/create-session をご覧ください。

polar.checkouts.create() のフィールドリファレンス

SDK では camelCase を使用し、基盤となる HTTP API では snake_case を使用します。ユーザーの既存コードに合わせて選択してください。

SDK フィールド(camelCase)API フィールド(snake_case)必須備考
productsproductsはい商品 UUID の配列です。最初の商品がデフォルトで選択されます。
customerIdcustomer_idいいえ既存の Polar 顧客 ID です。
externalCustomerIdexternal_customer_idいいえ独自のユーザー ID です。該当する Polar 顧客がいない場合、この外部 ID を持つ顧客が作成されます。
customerEmailcustomer_emailいいえ
customerNamecustomer_nameいいえ
customerBillingNamecustomer_billing_nameいいえ
customerBillingAddresscustomer_billing_addressいいえ{ country: "US", line1, line2, city, state, postal_code }。住所を設定する場合は、country(ISO 3166-1 alpha-2)が必須です。
customerTaxIdcustomer_tax_idいいえ
customerIpAddresscustomer_ip_addressいいえ
customerMetadatacustomer_metadataいいえ作成された顧客にコピーされます。
isBusinessCustomeris_business_customerいいえデフォルトは false です。true の場合、完全な請求先住所と名前が必要です。
metadatametadataいいえ作成された注文/サブスクリプションにコピーされます。
customFieldDatacustom_field_dataいいえカスタムフィールドのスラッグ → 値のマップです。
discountIddiscount_idいいえ
allowDiscountCodesallow_discount_codesいいえデフォルトは true です。
requireBillingAddressrequire_billing_addressいいえデフォルトは false です。米国の顧客には常に必須です。
seatsseatsいいえ事前に設定するシート数です(シート単位の料金設定の場合のみ)。
minSeats / maxSeatsmin_seats / max_seatsいいえシート単位の料金設定の場合のみです。
amountamountいいえセント単位です。custom(価格を購入者が決める方式)の価格の場合のみ使用されます。
subscriptionIdsubscription_idいいえ既存の無料サブスクリプションをアップグレードします。
successUrlsuccess_urlいいえ下記を参照してください。
returnUrlreturn_urlいいえチェックアウトページの戻るボタンの URL です。
embedOriginembed_originいいえチェックアウトを iframe に埋め込む場合に設定します。
localelocaleいいえIETF BCP 47(en、en-US、fr-CA など)です。デフォルトは en-US です。
currencycurrencyいいえISO 4217(例:usd、eur)です。
trialInterval / trialIntervalCounttrial_interval / trial_interval_countいいえ商品のトライアル設定を上書きします。
allowTrialallow_trialいいえデフォルトは true です。false に設定すると、別途設定されているトライアルを無効にできます。
pricespricesいいえ商品 ID → アドホック価格のマップです(カタログ価格を上書きします)。

レスポンス: { id, url, client_secret, expires_at, status, ... }。ユーザーを url にリダイレクトしてください。client_secret が必要なのは、@polar-sh/checkout を使用してチェックアウトを埋め込む場合のみです。

特定のフィールドに関する注意事項
  • successUrl と {CHECKOUT_ID}。 Polar は文字列をそのまま置換します。URL 内の {CHECKOUT_ID} トークンは、リダイレクト時に実際のセッション ID に置き換えられます。公式 API ドキュメントの例では ?checkout_id={CHECKOUT_ID} を使用していますが、任意のパラメーター名を使用できます。成功ページで polar.checkouts.get(checkoutId) を使ってセッションを取得する場合に便利です。
  • theme(API フィールドではなく、UI のヒント)。 ホステッドチェックアウトの UI では、URL に ?theme=light|dark を指定できます。テーマを適用するには、セッションの作成後に result.url に追加してください。これは polar.checkouts.create() のフィールドではありません。
標準実装(Web 標準 — リクエスト/レスポンス)

Deno、Bun、Cloudflare Workers、Supabase Edge Functions、SvelteKit、Remix、TanStack Start、Astro エンドポイント、Next.js Route Handlers(NextRequest は Request を継承)で、そのまま動作します。

import { Polar } from "@polar-sh/sdk";

const polar = new Polar({
  accessToken: process.env.POLAR_ACCESS_TOKEN,
  server: "production",
});

const SUCCESS_URL = "https://example.com/success?checkoutId={CHECKOUT_ID}";
const THEME: "light" | "dark" | undefined = undefined;

export async function GET(request: Request): Promise<Response> {
  const url = new URL(request.url);
  const products = url.searchParams.getAll("products");

  if (products.length === 0) {
    return Response.json(
      { error: "Missing products in query params" },
      { status: 400 },
    );
  }

  const json = (key: string) =>
    url.searchParams.has(key)
      ? JSON.parse(url.searchParams.get(key) ?? "{}")
      : undefined;
  const str = (key: string) => url.searchParams.get(key) ?? undefined;

  try {
    const result = await polar.checkouts.create({
      products,
      successUrl: SUCCESS_URL,
      customerId: str("customerId"),
      externalCustomerId: str("customerExternalId"),
      customerEmail: str("customerEmail"),
      customerName: str("customerName"),
      customerBillingAddress: json("customerBillingAddress"),
      customerTaxId: str("customerTaxId"),
      customerIpAddress: str("customerIpAddress"),
      customerMetadata: json("customerMetadata"),
      metadata: json("metadata"),
      discountId: str("discountId"),
      allowDiscountCodes: url.searchParams.has("allowDiscountCodes")
        ? url.searchParams.get("allowDiscountCodes") === "true"
        : undefined,
      seats: url.searchParams.has("seats")
        ? Number.parseInt(url.searchParams.get("seats") ?? "1", 10)
        : undefined,
    });

    const redirectUrl = new URL(result.url);
    if (THEME) redirectUrl.searchParams.set("theme", THEME);

    return Response.redirect(redirectUrl.toString(), 302);
  } catch (error) {
    console.error(error);
    return new Response(null, { status: 500 });
  }
}

このハンドラーは ?products=<product_id> を含む GET リクエストを受け付け、任意の顧客事前入力パラメーターをそのまま polar.checkouts.create() に渡します。呼び出し元が決して指定しないフィールドは削除し、必要に応じて上の表にあるフィールド(metadata、embedOrigin、locale など)を追加してください。フロントエンドがサーバーサイドでレンダリングされ、ユーザーがすでに特定されている場合は、クエリから読み取るのではなく、認証コンテキストから externalCustomerId を固定値として設定してください。

フレームワークごとのバリエーション

ハンドラーの本体は同じで、シグネチャ、URL の取得方法、レスポンスのコンストラクターのみが異なります。

Next.js(App Router): 標準ハンドラーを app/api/checkout/route.ts に配置します。NextRequest/NextResponse は省略可能で、request: Request を使用できます。

Express:

import type { Request, Response } from "express";

app.get("/checkout", async (req: Request, res: Response) => {
  const url = new URL(`${req.protocol}://${req.get("host")}${req.originalUrl}`);
  // ... same body using `url.searchParams` ...
  // res.redirect(redirectUrl.toString());
  // res.status(400).json({ error: "..." });
});

Fastify:

fastify.get("/checkout", async (request, reply) => {
  const url = new URL(`${request.protocol}://${request.hostname}${request.url}`);
  // ... same body ...
  // reply.redirect(redirectUrl.toString());
});

Hono:

app.get("/checkout", async (c) => {
  const url = new URL(c.req.url);
  // ... same body ...
  return c.redirect(redirectUrl.toString());
});

Elysia:

app.get("/checkout", ({ request, redirect }) => {
  const url = new URL(request.url);
  // ... same body, `return redirect(redirectUrl.toString())` ...
});

Nuxt(サーバールート): defineEventHandler、getQuery、sendRedirect(event, url, 302) を使用して server/api/checkout.get.ts に作成します。

SvelteKit: src/routes/api/checkout/+server.ts で GET({ request, url }) をエクスポートします。標準ハンドラーをそのまま使用できます。

Astro: src/pages/api/checkout.ts で export const GET: APIRoute = async ({ request }) => { ... } をエクスポートします。


レシピ 2 — カスタマーポータルのエンドポイント

目的: Polar の顧客セッションを作成し、ホストされたカスタマーポータル(サブスクリプション、請求書、支払い方法)へ 302 リダイレクトする、認証済みのエンドポイントです。

基盤となる API: POST /v1/customer-sessions/(SDK:polar.customerSessions.create(...))。完全なスキーマ:https://polar.sh/docs/api-reference/customer-portal/sessions/create

polar.customerSessions.create() のフィールドリファレンス

リクエストボディはユニオン型です。customerId または externalCustomerId のどちらか一方を渡し、両方は渡さないでください。

SDK フィールドAPI フィールド必須備考
customerIdcustomer_idどちらか一方既存の Polar 顧客 UUID です。
externalCustomerIdexternal_customer_idどちらか一方お客様独自のユーザー ID です。
returnUrlreturn_urlいいえポータルページの戻るボタンの URL です。
memberIdmember_idいいえmember_model_enabled が有効な組織でのみ使用します。Polar 顧客メンバーの UUID です。個人顧客の場合は、オーナーメンバーがデフォルトになります。
externalMemberIdexternal_member_idいいえmemberId の代わりとなる、お客様独自のメンバー ID です。

レスポンス: { id, customer_portal_url, token, expires_at, customer, customer_id, return_url, ... }。ユーザーを customer_portal_url にリダイレクトします。リダイレクトせずに自分の UI 内でポータルデータを表示する場合は、token と customer が役立ちます。

標準実装
import { Polar } from "@polar-sh/sdk";

const polar = new Polar({
  accessToken: process.env.POLAR_ACCESS_TOKEN,
  server: "production",
});

const RETURN_URL = "https://example.com/account";

// Replace with your auth: read a cookie/JWT/session and return your user id.
async function getExternalCustomerId(request: Request): Promise<string | null> {
  const session = await getSession(request); // your auth helper
  return session?.userId ?? null;
}

export async function GET(request: Request): Promise<Response> {
  const externalCustomerId = await getExternalCustomerId(request);
  if (!externalCustomerId) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  try {
    const { customerPortalUrl } = await polar.customerSessions.create({
      externalCustomerId,
      returnUrl: RETURN_URL,
    });
    return Response.redirect(customerPortalUrl, 302);
  } catch (error) {
    console.error(error);
    return new Response(null, { status: 500 });
  }
}

お客様独自のユーザー ID ではなく Polar 顧客 ID をお持ちの場合は、polar.customerSessions.create({ ... }) の呼び出しで externalCustomerId を customerId に置き換えてください。通常は externalCustomerId を使用します。認証システムがすでにユーザー ID を把握しているためです。

フレームワークごとのバリエーション

Checkout と同じ構成で、リクエスト/レスポンスの慣習のみが異なります。フレームワークに応じた方法で認証状態を読み取り(Next.js:next/headers の cookies()、Express:req.user、Hono:c.get('user') など)、結果を polar.customerSessions.create に渡してください。


レシピ 3 — Webhook エンドポイント

目的: Polar の Webhook イベントを受信して署名を検証し、イベントごとのハンドラーに振り分けます。

仕様: Polar ダッシュボードで設定した、固定 URL の POST エンドポイントです。@polar-sh/sdk/webhooks の validateEvent を使用して、webhook-id、webhook-timestamp、webhook-signature ヘッダーを検証します。成功時は 200 を返し、署名が一致しない場合は 403 を返します。

標準実装
import { validateEvent, WebhookVerificationError } from "@polar-sh/sdk/webhooks";

const WEBHOOK_SECRET = process.env.POLAR_WEBHOOK_SECRET!;

export async function POST(request: Request): Promise<Response> {
  const body = await request.text(); // must be the raw body, not parsed JSON

  let event: ReturnType<typeof validateEvent>;
  try {
    event = validateEvent(
      body,
      {
        "webhook-id": request.headers.get("webhook-id") ?? "",
        "webhook-timestamp": request.headers.get("webhook-timestamp") ?? "",
        "webhook-signature": request.headers.get("webhook-signature") ?? "",
      },
      WEBHOOK_SECRET,
    );
  } catch (error) {
    if (error instanceof WebhookVerificationError) {
      return Response.json({ received: false }, { status: 403 });
    }
    throw error;
  }

  switch (event.type) {
    case "checkout.created":
    case "checkout.updated":
      // event.data is the Checkout
      break;

    case "order.created":
    case "order.updated":
    case "order.paid":
    case "order.refunded":
      // event.data is the Order — fulfill, send receipt, etc.
      break;

    case "subscription.created":
    case "subscription.updated":
    case "subscription.active":
    case "subscription.canceled":
    case "subscription.uncanceled":
    case "subscription.revoked":
      // event.data is the Subscription — flip entitlements in your DB
      break;

    case "refund.created":
    case "refund.updated":
      break;

    case "product.created":
    case "product.updated":
      break;

    case "benefit.created":
    case "benefit.updated":
      break;

    case "benefit_grant.created":
    case "benefit_grant.updated":
    case "benefit_grant.revoked":
      // grant or revoke the user's access to a benefit
      break;

    case "customer.created":
    case "customer.updated":
    case "customer.deleted":
    case "customer.state_changed":
      break;

    case "organization.updated":
      break;
  }

  return Response.json({ received: true });
}

event には完全な型情報が付与されているため、各 case の中で TypeScript が event.data の型を絞り込みます。

重要な詳細
  1. validateEvent には生のリクエストボディを使用してください。フレームワークが JSON を解析している場合(Express の express.json()、Fastify のデフォルトボディパーサーなど)は、このルートでは無効にするか、生のボディを手動で読み取ってください。再シリアライズされた JSON では署名の検証に失敗します。
  2. すばやく応答してください。 Polar は、ステータスコードが 2xx 以外の場合やタイムアウトした場合に再試行します。ハンドラーの処理に時間がかかる場合(メール送信、在庫同期など)は、キュー(キュー/cron/バックグラウンドジョブ)に追加して、すぐに 200 を返してください。
  3. 冪等性を確保してください。 Polar はイベントを再配信する場合があります。webhook-id ヘッダーを使って重複を排除してください。この値は配信ごとに一意で、再試行時には同じ値が使われます(Standard Webhooks 仕様)。event.data.id で重複を排除しないでください。これはリソース ID であり、同じリソースに関する別々のイベント(例:order.created と order.paid)で共有されます。
  4. ルートを公開状態にしてください。 Polar はリダイレクトに従わないため、ログインページにリダイレクトする認証ミドルウェアの背後にあるルートでは、配信が毎回失敗します。認証には署名を使用してください。
ローカルでテストする

開発サーバーの実行中に、Polar CLI でイベントを転送し、1 件送信します(polar-testing の「Local Webhook Testing」で設定します)。

polar listen 3000/api/polar/webhook   # keep it running in the background
polar trigger order.paid              # exits 1 and prints the response when the handler fails

ローカルでは、POLAR_WEBHOOK_SECRET は polar listen --print-secret で取得したシークレットです。

フレームワークごとの違い

Next.js Route Handler: 標準のハンドラーを app/api/polar/webhook/route.ts に配置します。App Router はボディを自動解析しないため、request.text() が適切です。

Express: このルートに限り、生のボディパーサーを使ってマウントします。

app.post("/polar/webhook", express.raw({ type: "application/json" }), async (req, res) => {
  const body = (req.body as Buffer).toString("utf8");
  // ... validateEvent / switch ...
  res.json({ received: true });
});

Fastify: 生のボディを保持するコンテンツタイプパーサーを追加するか、@fastify/raw-body と request.rawBody を使用します。

Hono: await c.req.text() で生のボディを取得し、c.req.header("webhook-id") でヘッダーを読み取ります。

Elysia: 標準の Request で await request.text() を使用します。

SvelteKit: +server.ts で await event.request.text() を使用します。

Astro: APIRoute で await request.text() を使用します。

Nuxt: h3 の readRawBody(event) を使用します。


  • @polar-sh/<framework> パッケージ(例:@polar-sh/express)はインストールしないでください。非推奨です。これらのレシピで必要なのは @polar-sh/sdk だけです。例外は、Next.js App Router 用の @polar-sh/nextjs と Better Auth 用の @polar-sh/better-auth です。

コメント