0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Laravelでfincodeを使ったクレジットカード決済機能を実装

0
Last updated at Posted at 2025-11-04

はじめに

先日、実務でfincodeを使ったクレジットカード決済機能の実装を担当したので、その内容を共有します。初めてfincodeやLaravelAPI開発を行う方向けの内容となっているので、簡単な内容ではありますが、ご了承ください。

fincodeとは

APIを使ってシステムに決済手段を組み込むことができる、オンライン決済プラットフォームです。

公式チュートリアルでは、以下のように説明されています。

fincodeの立ち位置と役割

fincodeは、「オンライン上で提供されるバーチャルなレジ」のようなものです。
fincodeは、事業を行う皆さんと、決済手段を提供する決済会社( カード決済の場合はカード会社)の間に立ち、両者をつなぐ「ゲートウェイ」の役割を果たします。

2022年にローンチされた、比較的新しい決済サービスです。

APIによる柔軟な実装が可能で、様々な決済手段(クレジットカード決済、コンビニ決済、PayPay、口座振替など)に対応していることが特徴です。

新規ユーザー登録、APIキー確認

fincodeを用いたシステム開発を行う際は、新規ユーザー登録を行い、APIキーを取得する必要があります。

新規ユーザー登録

テスト環境

テスト環境新規ユーザー登録

エンジニアが開発を行う時は、テスト環境のアカウントを用います。実際の決済は行われないので、安心して開発することができます。

本番環境

本番環境新規ユーザー登録

本番環境は、実際のカード決済・入金処理が行われます。実運用で用います。ログイン時に二要素認証が必要です。また、本番環境申請が完了することで、APIが利用できるようになります。

APIキー確認

テスト環境

テスト環境のAPI情報、接続情報を確認する

ダッシュボードで新規ユーザー作成・ログインを行い、サイドメニューの「API・Webhook」を押下するとAPIキーを確認できます。ここで取得したAPIキー(パブリックキー、シークレットキー)は、後ほど.envに記載します。

fincodeAPIキー確認.png

本番環境

本番環境のAPI情報、接続情報を確認する

APIキーの確認方法はテスト環境と同じです。

fincodeの実装パターン

リダイレクト型を使用

fincodeが提供する決済画面を使用する方法です。決済画面を自前で作らなくて良いので、実装コストは最小です。簡易な決済機能で問題ないのであれば、リダイレクト型決済で十分でしょう。後ほど、こちらの方法を用いた実装例を紹介します。

リダイレクト型決済を実行する

リダイレクト型 決済画面

リダイレクト型.png

自サイトで実装

リダイレクト型決済を使わず、自サイト内で決済画面・決済機能を実装する方法です。フロントエンド・バックエンドの両方で、決済処理の実行が可能です。

  • フロントエンドで決済実行
    • バックエンドで決済登録を行い、その後フロントエンドで決済処理を実行する(決済JS)
  • バックエンドで決済実行
    • 新規カードの場合、フロントエンドでカードトークンを発行し、バックエンドで決済処理を実行する(トークン決済)
    • 保存済みカードの場合、顧客idやカードidを用いて、バックエンドで決済処理を実行する

また、決済画面について、UIコンポーネント(fincodeが提供する入力UI)を自サイトの決済画面に埋め込むことで、実装コストを下げることができます。

公式ブログにて、実装例が紹介されているので、参考にしてみると良いかもしれません

リダイレクト型決済実装例

APIリファレンス確認

まずはAPIリファレンスで詳細な内容を確認します。

リダイレクト型決済 決済URL 作成

リファレンスを直接確認した方がわかりやすいですが、念の為以下に転記します。

POST /v1/sessions

Request
{
  "transaction": {
    "pay_type": [
      "Card"
    ],
    "amount": "1000",
    "order_id": "o_**********************",
    "tax": "100",
    "client_field_1": null,
    "client_field_2": null,
    "client_field_3": null
  },
  "card": {
    "job_code": "CAPTURE",
    "tds_type": "2",
    "tds2_type": "2",
    "td_tenant_name": "s_***********-ab123",
    "tds2_ch_acc_change": "20240101",
    "tds2_ch_acc_date": "20220101",
    "tds2_ch_acc_pw_change": "20230101",
    "tds2_nb_purchase_account": "9999",
    "tds2_payment_acc_age": "20231231",
    "tds2_provision_attempts_day": "999",
    "tds2_ship_address_usage": "20230930",
    "tds2_ship_name_ind": "01",
    "tds2_suspicious_acc_activity": "01",
    "tds2_txn_activity_day": "999",
    "tds2_txn_activity_year": "999",
    "tds2_three_ds_req_auth_data": null,
    "tds2_three_ds_req_auth_method": "01",
    "tds2_three_ds_req_auth_timestamp": "202205191234",
    "tds2_email": "string",
    "tds2_addr_match": "Y",
    "tds2_bill_addr_country": "392",
    "tds2_bill_addr_state": "13",
    "tds2_bill_addr_city": "渋谷区",
    "tds2_bill_addr_line_1": "道玄坂1-14-6",
    "tds2_bill_addr_line_2": "ヒューマックス渋谷ビル",
    "tds2_bill_addr_line_3": "7F",
    "tds2_bill_addr_post_code": "150-0043",
    "tds2_ship_addr_country": "392",
    "tds2_ship_addr_state": "13",
    "tds2_ship_addr_city": "渋谷区",
    "tds2_ship_addr_line_1": "道玄坂1-14-6",
    "tds2_ship_addr_line_2": "ヒューマックス渋谷ビル",
    "tds2_ship_addr_line_3": "7F",
    "tds2_ship_addr_post_code": "150-0043",
    "tds2_ship_ind": "01",
    "tds2_delivery_email_address": "email@example.com",
    "tds2_home_phone_cc": "81",
    "tds2_home_phone_no": "312345678",
    "tds2_mobile_phone_cc": "81",
    "tds2_mobile_phone_no": "9012345678",
    "tds2_work_phone_cc": "81",
    "tds2_work_phone_no": "312345678",
    "tds2_delivery_timeframe": "01",
    "tds2_pre_order_date": "20231231",
    "tds2_pre_order_purchase_ind": "01",
    "tds2_reorder_items_ind": "01",
    "tds2_recurring_expiry": "20231231",
    "tds2_recurring_frequency": "99",
    "tds2_gift_card_amount": "999999",
    "tds2_gift_card_count": "99",
    "tds2_gift_card_curr": "392"
  },
  "konbini": {
    "payment_term_day": "2",
    "konbini_reception_mail_send_flag": "1"
  },
  "paypay": {
    "job_code": "CAPTURE",
    "order_description": "Your Shop上での購入"
  },
  "virtualaccount": {
    "payment_term_day": "90",
    "virtualaccount_reception_mail_send_flag": "1",
    "reference_order_id": "o_**********************"
  },
  "success_url": "https://your-service.example.com/success",
  "cancel_url": "https://your-service.example.com/cancel",
  "expire": "2022/02/31 23:59:59.999",
  "shop_service_name": "Your Service",
  "guide_mail_send_flag": "1",
  "receiver_mail": "receiver-email@example.com",
  "mail_customer_name": "買物 太郎",
  "thanks_mail_send_flag": "1",
  "shop_mail_template_id": null
}
Response(200 リクエストに成功)
{
  "id": "lk_**********************",
  "link_url": "https://secure.test.fincode.jp/v1/links/lk_**********************",
  "success_url": "https://your-service.example.com/success",
  "cancel_url": "https://your-service.example.com/cancel",
  "status": "CREATE",
  "expire": "2022/02/31 23:59:59.999",
  "shop_service_name": "Your Service",
  "guide_mail_send_flag": "1",
  "receiver_mail": "receiver-email@example.com",
  "mail_customer_name": "買物 太郎",
  "thanks_mail_send_flag": "0",
  "shop_mail_template_id": null,
  "transaction": {
    "pay_type": [
      "Card"
    ],
    "order_id": "o_**********************",
    "amount": 1000,
    "tax": 1000,
    "client_field_1": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore",
    "client_field_2": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore",
    "client_field_3": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore"
  },
  "card": {
    "job_code": "CAPTURE",
    "tds_type": "2",
    "td_tenant_name": "s_***********-ab123",
    "tds2_type": "2",
    "item_code": null
  },
  "konbini": {
    "konbini_reception_url": "https://secure.test.fincode.jp/v1/links/lk_**********************/konbini",
    "payment_term_day": 2,
    "konbini_reception_mail_send_flag": "1"
  },
  "paypay": {
    "job_code": "CAPTURE",
    "order_description": "Your Shop上での購入"
  },
  "virtualaccount": {
    "virtualaccount_reception_url": "https://secure.test.fincode.jp/v1/links/lk_**********************/virtualaccount",
    "payment_term_day": 90,
    "virtualaccount_reception_mail_send_flag": "1"
  },
  "bill_id": "string",
  "created": "2022/05/16 23:59:59.999",
  "updated": "2022/05/16 23:59:59.999"
}
Response(400 不正なリクエスト)
{
  "errors": [
    {
      "error_code": "E**********",
      "error_message": "string"
    }
  ]
}

リクエストについて、transaction(決済共通項目)のamount(決済金額)が必須項目です。

事前準備

.envの設定

ダッシュボードで取得したAPIキーを.envに記載します。ベースURLはなくても良いです。

.env
FINCODE_BASE_URL=https://api.test.fincode.jp  # 本番環境は https://api.fincode.jp
FINCODE_PUBLIC_KEY=p_test_***************     # パブリックキー
FINCODE_SECRET_KEY=m_test_***************     # シークレットキー

パブリックキーはフロントエンドで使います。fincodeJS(トークンJS)を用いて、カード情報のトークン化や決済JSの起動に利用します。
シークレットキーはバックエンドで使います。サーバーからfincodeAPIを呼び出す際の認証に用います。

設定キャッシュ

本番環境で設定キャッシュを使用することを想定し、環境変数はenv()で直接参照せず、config()で設定ファイルから取得するようにします。

config/services.php
<?php

return [
    'fincode' => [
        'base_url'   => env('FINCODE_BASE_URL'),
        'public_key' => env('FINCODE_PUBLIC_KEY'),
        'secret_key' => env('FINCODE_SECRET_KEY'),
    ],
];

例えば、コントローラーやサービスクラスで、config('services.fincode.secret_key')と書くことで、.envのシークレットキーを取得することができます。

ルーティング

routes/web.php
<?php

use App\Http\Controllers\PaymentConfirmController;
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;

Route::prefix('payment')->name('payment.')->group(function () {
    // 決済内容確認画面表示
    Route::match(['GET', 'POST'], '/confirm', [PaymentConfirmController::class, 'index'])
        ->name('confirm')->withoutMiddleware([ValidateCsrfToken::class]);

    // 決済URL 作成 APIを実行し、決済画面へリダイレクト
    Route::post('/confirm/store', [PaymentConfirmController::class, 'store'])
        ->name('confirm.store');

    // 決済完了画面表示
    Route::post('/complete', [PaymentConfirmController::class, 'index'])
        ->name('complete')->withoutMiddleware([ValidateCsrfToken::class]);
});

GETとPOSTの両方を許可

決済内容確認画面の表示について、GETとPOSTの両方を想定し、match()メソッドを使用しています。GETは通常のケース、POSTはリダイレクト型URLにてキャンセルされた時のケースを想定しています。キャンセル時はPOSTで決済内容確認画面へリダイレクトするので、POSTも許可しなければなりません。

POSTを許可しないと...

fincode決済画面からPOSTでリダイレクトした時、以下のエラーが発生します。

Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException

The POST method is not supported for route payment/confirm.
Supported methods: GET, HEAD.

POSTが許可されていないのにPOSTリクエストを送っているため、このようなエラーが発生します。

決済完了画面表示についても、画面表示なのでGETを使いたいところですが、リダイレクト型URLにて決済完了した時にPOSTでリダイレクトするため、post()メソッドを使用しています。

CSRFトークンを除外

fincode決済画面は外部サイトなので、そこからPOSTでリダイレクトした時、CSRFトークンを含みません。CSRFトークンがないとエラーが発生したり、想定外の挙動が起きたりします。そこで、withoutMiddleware()メソッドを使用し、引数にValidateCsrfTokenミドルウェアを指定することで、CSRFトークンを除外できます。CSRFトークン除外については、下記の記事がわかりやすかったです。

コントローラ

app/Http/Controllers/PaymentConfirmController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Http\RedirectResponse;
use Exception;

final class PaymentConfirmController extends Controller
{
    /**
     * fincodeが提供する決済画面へリダイレクト
     *
     * @param Request $request
     * @return RedirectResponse
     */
    public function store(Request $request): RedirectResponse
    {
        try {
            // 決済金額を取得
            // $amount = ??????;

            // リクエストボディを準備
            $requestData = [
                'transaction' => [
                    'pay_type' => 'Card',  // 決済手段:カード決済
                    'amount'   => $amount, // 決済金額
                ],
                'card' => [
                    'job_code'  => 'CAPTURE', // 取引種別:即時売上
                    'tds_type'  => '2',       // 3Dセキュア認証を利用するか。:3Dセキュア2.0認証を利用する
                    'tds2_type' => '2',       // 3Dセキュア2.0非対応時の挙動設定:エラーをレスポンスし、処理を終了する。
                ],
                'success_url' => route('payment.complete'), // 成功時リダイレクトURL
                'cancel_url'  => route('payment.confirm'),  // キャンセル時リダイレクトURL
            ];

            // 決済URL 作成 APIを実行
            $response = Http::baseUrl(config('services.fincode.base_url')) // fincodeのAPIエンドポイントへアクセス
                ->withToken(config('services.fincode.secret_key'))         // Authorization: Bearer <シークレットキー>
                ->acceptJson()                                             // レスポンスをapplication/jsonで返すように指定
                ->asJson()                                                 // リクエストボディをJSONで送る
                ->post("/v1/sessions", $requestData)                       // 決済URL 作成 APIをPOSTで実行
                ->throw();                                                 // エラーが発生した場合は例外を投げる

            // リダイレクト型決済URLへリダイレクト
            return redirect()->away($response->json()['link_url']);
        } catch (Exception $e) {
            // 例外をログに出力
            Log::error('fincode redirect failed: ' . $e->getMessage(), [
                'file'  => $e->getFile(),
                'line'  => $e->getLine(),
            ]);

            // 前のページにリダイレクトし、エラーメッセージを表示
            return back()->with('error', '決済処理に失敗しました。再度お試しください。');
        }
    }
}

リクエストボディ

$requestDataで、決済URL 作成 APIのリクエストボディを配列で作成しています。APIリファレンスに沿って、各項目の値を設定しています。

3Dセキュア2.0認証

オンラインカード決済の不正利用防止のための本人認証システムです。今回は、3Dセキュア2.0認証を利用する設定にしています。

リダイレクトURL

決済画面に配置されるボタンのリダイレクト先のことです。

fincode決済画面_リダイレクトURL.png

決済URL 作成 APIを実行

LaravelのHTTPクライアントを使用して、fincodeへのAPIを実行しています。HTTPクライアントで使用している各メソッドは、公式LaravelAPIリファレンス
で確認できます。

  • baseUrl():Set the base URL for the pending request.(このリクエスト設定に対して使用する基底URLを指定します。)
  • withToken():Specify an authorization token for the request.(このリクエストに使用する認可トークンを指定します。)
  • acceptJson():Indicate that JSON should be returned by the server.(サーバーにJSONでレスポンスを返すよう要求します〔Accept: application/jsonを付与〕。)
  • asJson():Indicate the request contains JSON.(このリクエストの本文をJSONとして送信することを指定します〔Content-Type: application/json・配列はJSONにエンコード〕。)
  • post():Issue a POST request to the given URL.(指定したURLへPOSTリクエストを送信します。)
  • throw():Throw an exception if a server or client error occurs.(サーバー/クライアントエラーが発生した場合に例外を投げます。)

外部ドメインへのリダイレクト

外部のページへリダイレクトするため、away()メソッドを使用しています。

自サイトでの決済実装

自サイトで決済処理を実装する(リダイレクト型決済を使用しない)場合、決済機能を柔軟にカスタマイズ可能です。私は、バックエンドで決済を実行する処理を実装しました。その処理のフローチャートを共有します。

雑ですが、以下の仕様を想定しています。

  • ゲストユーザーとログインユーザー、共にカード決済が可能
  • ゲストユーザーの場合
    • fincodeJSで生成したカードトークンでの決済処理を行う
  • ログインユーザーの場合
    • 初めてカード決済行う場合、顧客登録を行い、顧客idをDBに保存する
    • 初めて利用するカードで決済を行う場合、カード登録を行い、カードidをDBに保存する
    • 既に保存済みのカードを再度利用する場合、DBに保存された顧客idとカードidを使用して、決済処理を進める
  • 3Dセキュア2.0認証を使用する

決済処理_フローチャート.png

データ入力・データ出力については、省略しています。使用したAPIは以下の6つです。

  • あくまで一例なので、他にも様々な方法があると思います
  • コード量が多いので、すみませんが詳細なソースコード例は省きます

さいごに

初めて実務で、外部APIを使用した実装や決済機能の実装を行ったので、最初はかなり躓きました。公式リファレンスを読み込んだり、Googleで調べたり、AIを頼ったりすることで、なんとか形になりました。自分にとっては難しい内容でしたが、やってみて良かったと感じています。
fincode自体は、すごく使いやすかったです。APIで柔軟に実装ができて、リファレンスも理解しやすかったので、普通にオススメです。Stripeなど決済プラットフォームは色々ありますが、決済機能を実装する際は選択肢の1つとして考えても良いと思います。
本記事に関して、もし間違いがあればすみません。間違いがある場合は、コメントにてご指摘いただけると幸いです。
ここまで読んでいただき、ありがとうございました!

参考URL

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?