> ## Documentation Index
> Fetch the complete documentation index at: https://docs.waffo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# サインイン済みの顧客にメール認証なしで注文を管理してもらう

> 自社サイトでサインイン済みの顧客を、ストアのカスタマーポータルに直接送ります

## 構築するもの

自社サイトに**注文を管理**ボタンを置きます。サインイン済みの顧客がクリックすると、サーバーが Waffo Pancake にポータルリンクを要求し、ブラウザをリダイレクトします。顧客はメールアドレスや確認コードを入力することなく、サインイン済みの状態でストアのカスタマーポータルに入れます。

```mermaid theme={"system"}
sequenceDiagram
    participant C as Customer Browser
    participant S as Your Server
    participant W as Waffo Pancake API
    participant P as Customer Portal

    C->>S: Click "Manage orders"
    S->>S: Read the customer from your own session
    S->>W: Create portal link (API Key)
    W->>S: portalUrl, expiresAt
    S->>C: 302 redirect to portalUrl
    C->>P: Portal for this customer, this store
```

ポータルで顧客ができること：

| 領域 | 利用できる操作 |
| - | - |
| 注文と支払い | 閲覧 |
| サブスクリプション | 閲覧、キャンセル、再開、プラン変更 |
| 請求書 | 閲覧 |
| 返金チケット | 閲覧のみ —— このバージョンでは返金リクエストの作成・再提出はできません |

<Note>
  この方法で入った顧客は、ポータルから返金をリクエストできません。返金リクエストは自社のサポート窓口で受け付け、Dashboard または [返金チケットの作成](/ja/api-reference/endpoints/refunds/create-refund-ticket) API で返金してください。
</Note>

***

## 前提条件

* API Key（Dashboard → API & Development）。開発中はテストキーを使います
* 顧客に表示するストアの Store ID（`STO_xxx`）
* [認証済みチェックアウト](/ja/integrate/sdks#認証済みチェックアウト（推奨）)で作成され、`buyerIdentity` を持つ注文
* 自社のサインイン機能：このフローはサーバーのセッションを信頼するため、顧客は自社サイトで認証済みである必要があります
* SDK を使う場合：`@waffo/pancake-ts` 0.26.0 以降、`@waffo/pancake-nextjs` 0.9.0 以降、または [Go SDK](https://pkg.go.dev/github.com/waffo-com/waffo-pancake-sdk-go) v0.17.0 以降
* [ストア設定](/ja/settings/store-settings)でストアの **Website** が登録済みであること。ポータルのセッションが終了すると、ポータルはこのアドレスだけにリンクする**ショップのサイトに戻る**ボタンを表示します。ウェブサイトが未登録の場合、ボタンは表示されません

***

## ステップ 1：顧客 ID を決める

ポータルには、指定したストアで `buyerIdentity` がここで渡す値と一致する注文が表示されます。**注文作成時に渡すのと同じ、安定した値**を使ってください。

* **推奨：** 社内の顧客 ID —— 安定していて、別の人に再割り当てされない値
* メールアドレスは避けてください：顧客がメールを変えると過去の注文が見えなくなり、再利用されたアドレスは別の人に注文を見せてしまう恐れがあります
* 匿名で、または別の ID で行われた注文は**表示されません**。Waffo Pancake は ID を統合しません。識別子を切り替えた場合、それ以前の注文は古い値のままです

<Warning>
  `buyerIdentity` と `storeId` は、サーバー自身のサインインセッションから取得してください —— ブラウザが設定できるクエリパラメータ、フォーム項目、ヘッダー、Cookie の値から取ってはいけません。`buyerIdentity` を操作できる人は、その顧客のポータルを手に入れます。Waffo Pancake はサーバーを信頼し、顧客のメールアドレスを検証しません。
</Warning>

***

## ステップ 2：サーバーでリンクを作成する

API はバックエンドからのみ呼び出してください。API Key をブラウザに渡してはいけません。

* **環境は API Key に従います。** テストキーならテストデータ、本番キーなら本番データでポータルが開きます。環境パラメータはありません
* **クリックのたびにリンクを作成**し、リダイレクトの直前に発行してください。キャッシュ・保存・メール送信はしないでください
* **302 でリダイレクト** し、リダイレクトレスポンスに `Cache-Control: no-store` と `Referrer-Policy: no-referrer` を付けてください。トークン付きリンクがキャッシュされたり、referrer として渡されたりするのを防ぎます

<CodeGroup>
  ```typescript Next.js (Route Handler) theme={"system"}
  // app/account/portal/route.ts
  import { NextResponse } from "next/server";
  import { WaffoPancake } from "@waffo/pancake-ts";
  import { getSignedInUser } from "@/lib/auth"; // your own session check

  const client = new WaffoPancake({
    merchantId: process.env.WAFFO_MERCHANT_ID!,
    privateKey: process.env.WAFFO_PRIVATE_KEY!,
  });

  export async function GET(request: Request) {
    const user = await getSignedInUser(request);
    if (!user) {
      return NextResponse.redirect(new URL("/login", request.url), 302);
    }

    try {
      // portalUrl already includes #token=...
      const { portalUrl } = await client.auth.createCustomerPortalLink({
        storeId: process.env.WAFFO_STORE_ID!,
        buyerIdentity: user.id, // same value used at checkout
      });

      const response = NextResponse.redirect(portalUrl, 302);
      response.headers.set("Cache-Control", "no-store");
      response.headers.set("Referrer-Policy", "no-referrer");
      return response;
    } catch (err) {
      // Log the error, never the portal URL
      console.error("customer portal link failed", err);
      return new NextResponse("The customer portal is unavailable. Please try again.", { status: 502 });
    }
  }
  ```

  ```typescript Next.js (SDK helper) theme={"system"}
  // app/account/portal/route.ts
  // Signature summary — see the @waffo/pancake-nextjs reference for the exact types
  import { createCustomerPortalRedirect } from "@waffo/pancake-nextjs/server";
  import { getSignedInUser } from "@/lib/auth"; // your own session check

  export const GET = createCustomerPortalRedirect(
    {
      merchantId: process.env.WAFFO_MERCHANT_ID!,
      privateKey: process.env.WAFFO_PRIVATE_KEY!,
    },
    {
      // Resolve the customer from YOUR session, never from the request body or query
      resolveAuthenticatedCustomer: async (request) => {
        const user = await getSignedInUser(request);
        return user ? { storeId: process.env.WAFFO_STORE_ID!, buyerIdentity: user.id } : null;
      },
      // Fixed path you control; without loginUrl, signed-out visitors get a 401
      loginUrl: "/login?returnTo=/account/portal",
    },
  );
  ```

  ```typescript Node.js (Express) theme={"system"}
  import express from "express";
  import { WaffoPancake } from "@waffo/pancake-ts";

  const client = new WaffoPancake({
    merchantId: process.env.WAFFO_MERCHANT_ID!,
    privateKey: process.env.WAFFO_PRIVATE_KEY!,
  });

  const app = express();

  // req.user is set by your own auth middleware; typed here for the example
  type SignedInRequest = express.Request & { user?: { id: string } };

  app.get("/account/portal", async (req: SignedInRequest, res) => {
    const user = req.user;
    if (!user) return res.redirect(302, "/login");

    try {
      const { portalUrl } = await client.auth.createCustomerPortalLink({
        storeId: process.env.WAFFO_STORE_ID!,
        buyerIdentity: user.id,
      });
      res.set("Cache-Control", "no-store");
      res.set("Referrer-Policy", "no-referrer");
      res.redirect(302, portalUrl);
    } catch (err) {
      // Log the error, never the portal URL
      console.error("customer portal link failed", err);
      res.status(502).send("The customer portal is unavailable. Please try again.");
    }
  });
  ```

  ```go Go theme={"system"}
  // Snippet, not a full program: mount portalHandler on your own server.
  // signedInUser is your own session lookup, e.g.
  //   func signedInUser(r *http.Request) (user User, ok bool)
  // Exact types: https://pkg.go.dev/github.com/waffo-com/waffo-pancake-sdk-go
  import (
  	"log"
  	"net/http"
  	"os"

  	pancake "github.com/waffo-com/waffo-pancake-sdk-go"
  )

  func portalHandler(client *pancake.Client) http.HandlerFunc {
  	return func(w http.ResponseWriter, r *http.Request) {
  		user, ok := signedInUser(r) // your own session check
  		if !ok {
  			http.Redirect(w, r, "/login", http.StatusFound)
  			return
  		}

  		link, err := client.Auth.CreateCustomerPortalLink(r.Context(), pancake.CreateCustomerPortalLinkParams{
  			StoreID:       os.Getenv("WAFFO_STORE_ID"),
  			BuyerIdentity: user.ID, // same value used at checkout
  		})
  		if err != nil {
  			log.Printf("customer portal link failed: %v", err) // never log link.PortalURL
  			http.Error(w, "The customer portal is unavailable. Please try again.", http.StatusBadGateway)
  			return
  		}

  		// link.PortalURL already includes #token=...
  		w.Header().Set("Cache-Control", "no-store")
  		w.Header().Set("Referrer-Policy", "no-referrer")
  		http.Redirect(w, r, link.PortalURL, http.StatusFound)
  	}
  }
  ```
</CodeGroup>

SDK ヘルパー `createCustomerPortalRedirect` は次のオプションを受け取ります：

| オプション | 必須 | 動作 |
| - | - | - |
| `resolveAuthenticatedCustomer` | はい | 自社のセッションから `{ storeId, buyerIdentity }` を返します。誰もサインインしていない場合は `null` を返します |
| `loginUrl` | いいえ | 未サインインの訪問者を 302 で送る先。指定しない場合は `401` の JSON レスポンスを返します |
| `onError` | いいえ | リンク作成の失敗を処理します。デフォルトは汎用的な `502` の JSON レスポンスです。エラーのログ記録（ポータルリンクは記録しない）や独自ページの返却に使います |

ヘルパーが組み立てるレスポンスにはすべて `Cache-Control: no-store` と `Referrer-Policy: no-referrer` が付きます。`onError` から返すレスポンスは自社の責任なので、同じく両方のヘッダーを付けてください。

<Warning>
  `loginUrl` には自社で管理するパスを設定してください。リクエストパラメータ（`returnTo` クエリの値など）をそのままコピーして組み立ててはいけません —— ポータルのルートがオープンリダイレクトになります。
</Warning>

SDK を使わない場合は、`purpose: "portal"` と `storeId` を付けて[セッショントークンの発行](/ja/api-reference/endpoints/auth/issue-session-token)を呼び出し、`portalUrl + "#token=" + token` にリダイレクトします。

***

## ステップ 3：ボタンを追加する

ボタンは Waffo Pancake ではなく自社のルートを指すようにします。ブラウザが API Key を目にすることはなく、リンクは顧客がクリックしたときにだけ作成されます。

```tsx theme={"system"}
// A plain <a>, not next/link: prefetching would create portal links nobody clicked
const PORTAL_ROUTE = "/account/portal"; // the route from Step 2

export function ManageOrdersButton() {
  return <a href={PORTAL_ROUTE}>Manage orders</a>;
}
```

***

## セッションの動作

| 動作 | 詳細 |
| - | - |
| 有効期間 | 15 分間操作がないと失効します。ポータルでの操作ごとに延長されます |
| セッション切れ | ポータルは再入場ページを表示し、顧客に自社サイトへ戻るよう案内します。ストアのウェブサイトが登録されていれば**ショップのサイトに戻る**ボタンも表示されます。**注文を管理**をもう一度クリックすると新しいリンクが作成されます —— メールのコードは不要です |
| リンクの再利用 | リンクは**使い捨てではありません**：セッションが有効な間は再度開けます。パスワードと同様に扱ってください |
| ブラウザごとに 1 つのポータル | 1 つのブラウザが保持できるポータルセッションは同時に 1 つです。自社サイトから入ると、そのブラウザの以前のポータルセッション（メールのマジックリンクや別ストアで開いたものを含む）が置き換えられます。置き換えられたセッションの古いタブには**セッションが切り替わりました**と表示され、現在のセッションに移動します |
| 範囲 | 1 ストア、1 環境、1 つの `buyerIdentity`。他のストアや別の環境のデータは見えません |

<Warning>
  リンクの `#token=` 部分は bearer 認証情報です。リンクをログ、アナリティクス、エラートラッカー、サポートチケットに書き込まず、メールやチャットでも共有しないでください。
</Warning>

***

## 本番前のテスト

<Steps>
  <Step title="テスト注文を作成">
    テスト API Key を使い、テスト顧客の `buyerIdentity` で認証済みチェックアウトから注文します。
  </Step>

  <Step title="ポータルを開く">
    その顧客として自社サイトにサインインし、**注文を管理**をクリックします。テスト注文が表示されるはずです。
  </Step>

  <Step title="ID の境界を確認">
    別の顧客でサインインし、最初の顧客の注文が表示されないことを確認します。リクエストに他の顧客の ID を加えてみてください —— ルートはそれを無視しなければなりません。
  </Step>

  <Step title="本番に切り替え">
    本番 API Key でデプロイします。以降、リンクは本番データを開きます。
  </Step>
</Steps>

***

## トラブルシューティング

<AccordionGroup>
  <Accordion title="ポータルは開くが注文が表示されない">
    1. 注文が同じ `buyerIdentity` 文字列で作成されているか確認します（大文字小文字を含む完全一致）
    2. 注文が渡した `storeId` のものか確認します
    3. API Key の環境を確認します：テストキーはテスト注文のみ、本番キーは本番注文のみを表示します
    4. 匿名チェックアウトの注文は `buyerIdentity` を持たないため、ここには表示されません
  </Accordion>

  <Accordion title="ポータルにセッション終了と表示される">
    セッションが 15 分以上アイドル状態だった場合、または新しいリンクなしで、このブラウザのセッションとは別のストアのポータルを開いた場合、ポータルは**ショップのサイトから入り直してください**と表示します。自社サイトの**注文を管理**から入り直してもらい、新しいリンクを取得してください。
  </Accordion>

  <Accordion title="ポータルにセッションが切り替わったと表示される">
    顧客が同じブラウザで再度ポータルに入りました —— 別のタブで自社サイトから入った、別ストアのポータルに入った、またはメールのマジックリンクを使った場合です。最新の入場がそのブラウザのポータルセッションになり、古いタブには**セッションが切り替わりました**と表示され、現在のセッションに移動します。対応は不要です。
  </Accordion>

  <Accordion title="Customer Portal API から 403 Environment mismatch が返る">
    ポータルトークンで Customer Portal API を呼び出した際、`X-Environment` が API Key の環境と異なっています。キーの環境を送ってください。
  </Accordion>
</AccordionGroup>

***

## チェックリスト

* [ ] リンクはサーバーで作成し、API Key はブラウザに届かない
* [ ] `buyerIdentity` は自社のサインインセッションから取得し、チェックアウト時の値と一致する
* [ ] `storeId` はリクエストではなくサーバーの設定から取得する
* [ ] ルートは 302、`Cache-Control: no-store`、`Referrer-Policy: no-referrer` で応答する
* [ ] ポータルリンクとトークンをログに記録・保存しない
* [ ] 返金リクエストにはポータル外のサポート窓口がある

***

## 次のステップ

<CardGroup cols={2}>
  <Card title="セッショントークンの発行" icon="key" href="/ja/api-reference/endpoints/auth/issue-session-token">
    `purpose: "portal"` のリクエストとレスポンスのフィールド。
  </Card>

  <Card title="カスタマーポータル" icon="user" href="/ja/features/customer-portal">
    顧客がポータルで目にする内容。
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.