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

# サーバー間決済 (S2S)

> 自社の決済フォームでカード情報を収集し、APIで決済を処理する方法

## 概要

サーバー間決済（S2S）では、ホスト型チェックアウトページの代わりに、自社の決済フォームでカード情報を収集できます。カードデータはJavaScript SDKを使用してブラウザ上で安全にトークン化され、そのトークンをサーバーに送信して決済を作成します。

<Note>
  S2S統合にはPCI SAQ A-EP準拠が必要です。PCI準拠レベルが不明な場合は、[ホスト型チェックアウト](/ja/guide/card-payments)をご利用ください。
</Note>

## 仕組み

```mermaid theme={null}
sequenceDiagram
    participant B as ブラウザ
    participant S as 加盟店サーバー
    participant Z as ZAFA PAY API

    B->>Z: 1. トークン作成（JS SDK、公開キー）
    Z-->>B: tok_xxx
    B->>S: 2. トークンをサーバーに送信
    S->>Z: 3. トークンで決済作成（シークレットキー）
    Z-->>S: 決済結果
    S-->>B: 4. 結果を顧客に表示
```

<Steps>
  <Step title="ブラウザでカード情報をトークン化">
    **公開キー**（`pk_live_*` / `pk_test_*`）を使用して `POST /v1/tokens` でカード情報をトークン化します。JavaScript SDKまたはAPI直接呼び出しが使用できます。生のカード番号がサーバーに送信されることはありません。
  </Step>

  <Step title="決済を作成">
    `tok_*` トークンをバックエンドに送信し、**シークレットアクセストークン**を使用して `token` パラメータ付きで `POST /v1/payments` を呼び出します。
  </Step>

  <Step title="3Dセキュアの処理（必要な場合）">
    レスポンスのステータスが `requires_action` の場合、顧客を `redirect_url` にリダイレクトして3Dセキュア認証を完了させます。
  </Step>
</Steps>

## 統合手順

### 1. カード情報のトークン化

決済ページでカード情報を収集し、公開キーを使用して `POST /v1/tokens` に送信します。JavaScript SDKを使用するか、APIを直接呼び出すことができます。

<CodeGroup>
  ```javascript JavaScript SDK theme={null}
  // 読み込み: <script src="https://js.zafapay.com/v1/zafapay.js"></script>
  const zafapay = Zafapay('pk_test_xxxxx');

  try {
    const { token, card } = await zafapay.createToken({
      number: '4242424242424242',
      exp_month: 12,
      exp_year: 2027,
      cvc: '123',
      cardholder_name: 'John Doe'  // 任意
    });

    console.log(token);      // "tok_xxxxxxxxxxxxxxxxxxxxxx"
    console.log(card.brand);  // "visa"
    console.log(card.last4);  // "4242"

    // トークンをサーバーに送信
    await fetch('/your-server/pay', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token, amount: 1000, currency: 'jpy' })
    });
  } catch (error) {
    console.error(error.message);
  }
  ```

  ```javascript API直接呼び出し theme={null}
  try {
    const response = await fetch('https://api.sandbox.zafapay.com/v1/tokens', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer pk_test_xxxxx',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        card: {
          number: '4242424242424242',
          exp_month: 12,
          exp_year: 2027,
          cvc: '123',
          cardholder_name: 'John Doe'
        }
      })
    });

    const { id, card } = await response.json();
    // id: "tok_xxxxxxxxxxxxxxxxxxxxxx"

    // トークンをサーバーに送信
    await fetch('/your-server/pay', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token: id, amount: 1000, currency: 'jpy' })
    });
  } catch (error) {
    console.error(error.message);
  }
  ```
</CodeGroup>

**レスポンスのプロパティ:**

| フィールド            | 説明                                       |
| ---------------- | ---------------------------------------- |
| `id`             | トークンID（`tok_` プレフィックス）。有効期限30分、1回のみ使用可能。 |
| `card.last4`     | 下4桁                                      |
| `card.brand`     | カードブランド（`visa`, `mastercard`, `amex` 等）  |
| `card.exp_month` | 有効期限（月）                                  |
| `card.exp_year`  | 有効期限（年）                                  |
| `expires_at`     | トークンの有効期限（ISO 8601形式）                    |

<Info>
  JavaScript SDKは便宜上、トークンIDを `id` ではなく `token` として返します。APIを直接呼び出す場合、フィールド名は `id` です。
</Info>

### 2. トークンで決済を作成

サーバーサイドで、トークンを使って `POST /v1/payments` を呼び出します。

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.sandbox.zafapay.com/v1/payments \
    -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "amount": 1000,
      "currency": "jpy",
      "token": "tok_xxxxxxxxxxxxxxxxxxxxxx",
      "return_url": "https://your-site.com/payment-complete",
      "external_id": "order_12345"
    }'
  ```

  ```javascript Node.js theme={null}
  const response = await fetch('https://api.sandbox.zafapay.com/v1/payments', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      amount: 1000,
      currency: 'jpy',
      token: 'tok_xxxxxxxxxxxxxxxxxxxxxx',
      return_url: 'https://your-site.com/payment-complete',
      external_id: 'order_12345'
    })
  });

  const payment = await response.json();
  ```
</CodeGroup>

### 3. レスポンスの処理

レスポンスのステータスによって次のアクションが決まります：

| ステータス             | HTTPコード | アクション                                           |
| ----------------- | ------- | ----------------------------------------------- |
| `completed`       | 201     | 決済成功。成功ページを表示。                                  |
| `authorized`      | 201     | オーソリ成功（`capture_method: "manual"` の場合）。後でキャプチャ。 |
| `requires_action` | 202     | 3Dセキュアが必要。顧客を `redirect_url` にリダイレクト。           |
| `failed`          | 400     | 決済失敗。エラーを顧客に表示。                                 |

**3Dセキュアの処理:**

```javascript theme={null}
const payment = await response.json();

if (payment.status === 'requires_action') {
  // 顧客を3DS認証ページにリダイレクト
  window.location.href = payment.redirect_url;
  // 3DS完了後、顧客はreturn_urlにリダイレクトされます
}
```

<Warning>
  S2S決済では必ず `return_url` を指定してください。3Dセキュアが発生した場合、認証後に顧客はこのURLに `status=succeeded` または `status=failed` のクエリパラメータ付きでリダイレクトされます。`return_url` がない場合、3DS後のリダイレクト先がなくなります。
</Warning>

## カード保存と継続決済

S2Sトークン決済時に `save_card: true` と `customer_id` を追加することで、顧客のカード情報を保存できます。決済完了後（3Dセキュアを含む）、カードが保存され、Webhookで `payment_method_id` が返されます。

### リクエスト

```bash theme={null}
curl -X POST https://api.sandbox.zafapay.com/v1/payments \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 1000,
    "currency": "jpy",
    "token": "tok_xxxxxxxxxxxxxxxxxxxxxx",
    "customer_id": "cust_abc123",
    "save_card": true,
    "return_url": "https://your-site.com/payment-complete",
    "external_id": "order_12345"
  }'
```

<ParamField body="save_card" type="boolean">
  `true` を指定すると、決済完了後にカード情報が保存されます。
</ParamField>

<ParamField body="customer_id" type="string">
  `save_card` が `true` の場合は必須。顧客を識別する一意のID。
</ParamField>

### Webhook

決済完了時のWebhookに、保存された `payment_method_id` が含まれます：

```json theme={null}
{
  "event": "payment.succeeded",
  "transaction_id": "tx_abc123",
  "status": "succeeded",
  "amount": "1000",
  "currency": "jpy",
  "external_id": "order_12345",
  "payment_method": "card",
  "payment_method_id": "pmi_xyz789",
  "save_card": true,
  "is_recurring": false,
  "customer_id": "cust_abc123",
  "card_brand": "visa",
  "card_last4": "4242",
  "card_country": "JP",
  "card_funding": "credit",
  "metadata": {},
  "created_at": "2026-04-08T10:00:00.000Z",
  "timestamp": "2026-04-08T10:00:05.000Z"
}
```

### 2回目以降の継続決済

保存された `payment_method_id` を使用して、カード入力やトークン化なしで課金できます：

```bash theme={null}
curl -X POST https://api.sandbox.zafapay.com/v1/payments \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 1000,
    "currency": "jpy",
    "customer_id": "cust_abc123",
    "payment_method_id": "pmi_xyz789",
    "external_id": "subscription_renewal_002"
  }'
```

保存済みカードの管理や継続決済の失敗処理の詳細は、[継続決済（リカーリング）](/ja/guide/recurring-payments)をご覧ください。

## 完全な実装例

### クライアントサイド（ブラウザ）

```html theme={null}
<form id="payment-form">
  <input type="text" id="card-number" placeholder="カード番号" />
  <input type="text" id="card-expiry-month" placeholder="MM" />
  <input type="text" id="card-expiry-year" placeholder="YYYY" />
  <input type="text" id="card-cvc" placeholder="CVC" />
  <input type="text" id="cardholder-name" placeholder="カード名義" />
  <button type="submit">¥1,000 を支払う</button>
</form>

<script src="https://js.zafapay.com/v1/zafapay.js"></script>
<script>
const zafapay = Zafapay('pk_test_xxxxx');

document.getElementById('payment-form').addEventListener('submit', async (e) => {
  e.preventDefault();

  try {
    // 1. カードをトークン化
    const { token } = await zafapay.createToken({
      number: document.getElementById('card-number').value,
      exp_month: parseInt(document.getElementById('card-expiry-month').value),
      exp_year: parseInt(document.getElementById('card-expiry-year').value),
      cvc: document.getElementById('card-cvc').value,
      cardholder_name: document.getElementById('cardholder-name').value
    });

    // 2. トークンをサーバーに送信
    const response = await fetch('/api/pay', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token })
    });

    const payment = await response.json();

    // 3. 結果を処理
    if (payment.status === 'requires_action') {
      window.location.href = payment.redirect_url;
    } else if (payment.status === 'completed' || payment.status === 'authorized') {
      window.location.href = '/success';
    } else {
      alert('決済失敗: ' + payment.error?.message);
    }
  } catch (error) {
    alert('エラー: ' + error.message);
  }
});
</script>
```

### サーバーサイド

<CodeGroup>
  ```javascript Node.js (Express) theme={null}
  const express = require('express');
  const app = express();
  app.use(express.json());

  app.post('/api/pay', async (req, res) => {
    const { token } = req.body;

    // 本番環境では、金額・通貨を注文データベースから取得してください
    const response = await fetch('https://api.sandbox.zafapay.com/v1/payments', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        amount: 1000,
        currency: 'jpy',
        token,
        return_url: 'https://your-site.com/payment-complete',
        external_id: `order_${Date.now()}`
      })
    });

    const payment = await response.json();
    res.json(payment);
  });
  ```

  ```python Python (Flask) theme={null}
  from flask import Flask, request, jsonify
  import requests
  import time

  app = Flask(__name__)

  @app.route('/api/pay', methods=['POST'])
  def pay():
      token = request.json['token']

      # 本番環境では、金額・通貨を注文データベースから取得してください
      response = requests.post(
          'https://api.sandbox.zafapay.com/v1/payments',
          headers={
              'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
              'Content-Type': 'application/json'
          },
          json={
              'amount': 1000,
              'currency': 'jpy',
              'token': token,
              'return_url': 'https://your-site.com/payment-complete',
              'external_id': f'order_{int(time.time())}'
          }
      )

      return jsonify(response.json())
  ```

  ```php PHP theme={null}
  <?php
  $payload = json_decode(file_get_contents('php://input'), true);
  $token = $payload['token'];

  // 本番環境では、金額・通貨を注文データベースから取得してください
  $ch = curl_init('https://api.sandbox.zafapay.com/v1/payments');
  curl_setopt_array($ch, [
      CURLOPT_POST => true,
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER => [
          'Authorization: Bearer YOUR_ACCESS_TOKEN',
          'Content-Type: application/json'
      ],
      CURLOPT_POSTFIELDS => json_encode([
          'amount' => 1000,
          'currency' => 'jpy',
          'token' => $token,
          'return_url' => 'https://your-site.com/payment-complete',
          'external_id' => 'order_' . time()
      ])
  ]);

  $response = curl_exec($ch);
  curl_close($ch);

  header('Content-Type: application/json');
  echo $response;
  ```
</CodeGroup>

## 公開キー

公開キー（`pk_test_*` / `pk_live_*`）は、ブラウザからのトークン化リクエストの認証に使用されます。トークンの作成のみに使用でき、決済、顧客情報、その他のAPIリソースにはアクセスできません。

公開キーは加盟店管理画面の **加盟店設定 > API設定** から取得できます。

詳細は[認証](/ja/api-reference/authentication)をご覧ください。

## テストカード

| カード番号            | 説明       |
| ---------------- | -------- |
| 4242424242424242 | 決済成功     |
| 4000002500003155 | 3Dセキュア必要 |
| 4000000000000002 | 拒否       |
| 4000000000009995 | 残高不足     |

<Info>
  APIパラメータとレスポンスの詳細は、[決済作成](/api-reference/payments/create-payment) APIリファレンスおよび[トークン作成](/ja/api-reference/tokens) APIリファレンスをご覧ください。
</Info>
