How to Integrate Worldpay Payment Gateway for Subscriptions in Vue.js and Nuxt.js

By Ved Prakash N | Apr 10, 2026 | Vue JS
Share :

https://www.fundaofwebit.com/post/how-to-integrate-worldpay-payment-gateway-for-subscriptions-in-vue-js-and-nuxt-js

Step-by-Step Guide to Worldpay Subscription Integration in Vue.js & Nuxt.js


Step 1: Create a file named Worldpay-subscribe.vue on Vue JS or Nuxt JS Application and paste the below code in it.

In this code, we will create a session payment.

<template>

  <div class="px-4 bg-white">
    <div class="max-w-7xl mx-auto sm:px-6 lg:px-8 py-10">
      <div class="grid grid-cols-12">
        <div class="col-span-12 lg:col-span-6">
          <div class="mb-6 text-center md:text-start order-1">
            <h1 class="text-2xl md:text-4xl font-semibold">Start Your Subscription</h1>
            <p class="text-gray-500 text-xl md:text-4xl mb-6">pay now</p>
          </div>

          <div class="flex items-center mt-10 mb-6 order-2 md:order-3">
            <img src="/images/icons/credit-debit.png" alt="Card or Debit" class="w-[80px] rounded-xl me-3" />
            <p class="text-2xl font-bold text-gray-700">Card or Debit</p>
          </div>

          <section class="container" id="container">
            <section class="card">
              <section class="checkout" id="card-form">
                <div class="label">Card number <span class="type"></span></div>
                <section id="card-pan" class="field"></section>
                <section class="columns">
                  <section class="column">
                    <div class="label">Expiry date</div>
                    <section id="card-expiry" class="field"></section>
                  </section>
                  <section class="column">
                    <div class="label">CVV</div>
                    <section id="card-cvv" class="field"></section>
                  </section>
                </section>
                <section class="buttons">
                  <button class="submit" id="submit" type="button">
                    <span v-if="isSubmitting"> <i class="bx bx-loader bx-spin text-xl"></i> Submitting... </span>
                    <span v-else>Join</span>
                  </button>
                  <button class="clear" id="clear" type="button">
                    Clear
                  </button>
                </section>
              </section>
            </section>
          </section>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { onMounted } from 'vue';
import axios from 'axios';

const isSubmitting = ref(false);
let checkoutInstance = null

const loadScript = () => {
  return new Promise((resolve, reject) => {
    if (window.Worldpay) return resolve()

    const script = document.createElement('script')
    script.src = 'https://try.access.worldpay.com/access-checkout/v2/checkout.js'
    script.onload = resolve
    script.onerror = reject

    document.head.appendChild(script)
  })
}

const initialiseCheckout = async () => {
  const checkoutId = 'YOUR_WORLDPAY_CHECKOUT_ID'

  const containerSelector = '#container'
  const panParentSelector = '#card-pan'
  const cvvParentSelector = '#card-cvv'
  const expiryDateParentSelector = '#card-expiry'

  const submitButton = document.querySelector('#submit')
  const clearButton = document.querySelector('#clear')

  window.Worldpay.checkout.init(
    {
      id: checkoutId,

      form: containerSelector,

      fields: {
        pan: {
          selector: panParentSelector,
          placeholder: '4444 3333 2222 1111'
        },
        expiry: {
          selector: expiryDateParentSelector,
          placeholder: 'MM/YY'
        },
        cvv: {
          selector: cvvParentSelector,
          placeholder: '123'
        }
      },

      styles: {
        input: {
          color: 'black',
          fontWeight: 'bold',
          fontSize: '20px',
          letterSpacing: '3px'
        },
        'input.is-valid': {
          color: 'green'
        },
        'input.is-invalid': {
          color: 'red'
        }
      },

      enablePanFormatting: true,
      allowNonLuhnCompliantCards: true
    },
    (error, checkout) => {
      if (error) {
        console.error('Init error:', error)
        return
      }

      checkoutInstance = checkout

      submitButton?.addEventListener('click', (event) => {
        event.preventDefault()

        checkout.generateSessionState((error, sessionState) => {
          if (error) {
            console.error('Session error:', error)
            return
          }
          // console.log('SESSION:', sessionState)
          // alert(`Session: ${sessionState}`)
          axios.post('http://localhost:8000/api/worldpay/create-session-payment', {
              session_state: sessionState
          }).then((response) => {
            console.log(response.data)
            if(response.status === 200){
              window.location.href = '/users/my-accounts'
            }
          });
         
        })
      })

      clearButton?.addEventListener('click', (event) => {
        event.preventDefault()
        checkout.clearForm(() => {})
      })
    }
  )
}

onMounted(async () => {
  try {
    await loadScript()
    initialiseCheckout()
  } catch (err) {
    console.error('Script load failed:', err)
  }
})

</script>

<style scoped>
.payment-content{
    font-size: 15px;
}

.container {
    display: flex;
    align-items: center;
    flex-direction: column;
}
.columns {
    display: flex;
}
.columns .column {
    margin-right: 15px;
}
.field {
    height: 40px;
    border-bottom: 1px solid lightgray;
    /* border: 1px solid lightgray;
    border-radius: 4px;
    padding: 10px; */
}
.field.is-onfocus {
    border-color: black;
}
.field.is-empty {
    border-color: orange;
}
.field.is-invalid {
    border-color: red;
}
.field.is-valid {
    border-color: green;
}
#card-pan {
    margin-bottom: 30px;
}
.card .checkout .submit {
    background: #dc2626;
    cursor: pointer;
    width: 200px;
    margin-top:30px;
    color: white;
    outline: 0;
    font-size: 14px;
    border: 0;
    border-radius: 4px;
    text-transform: uppercase;
    font-weight: bold;
    padding: 15px 0;
    transition: background 0.3s ease;
    margin-right:20px;
}
.card .checkout .clear {
    background: green;
    cursor: pointer;
    width: 100px;
    margin-top:30px;
    color: white;
    outline: 0;
    font-size: 14px;
    border: 0;
    border-radius: 4px;
    text-transform: uppercase;
    font-weight: bold;
    padding: 15px 0;
    transition: background 0.3s ease;
}
.buttons {
    display: flex;
}
</style>


Let's move to Laravel as Backend.

Step 1: Create a route in api.php file as follows:

Route::post('/worldpay/create-session-payment', [PaymentController::class, 'createPaymentFromSession']);


Step 2: Create a Model named Membership.php and adjust the fields as per your requirement and create a migration too:

<?php

namespace App\Models;

use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class Membership extends Model
{
    use HasFactory;

    protected $table = 'memberships';

    protected $fillable = [
        'user_id',
        'joined_date',
        'expiry_date',
        'is_free_trail_completed',
        'worldpay_token',
        'cancelled_at',
        'status', // free_trail_active, active, free_trial_completed, cancelled, expired
    ];

    protected $casts = [
        'joined_date' => 'date',
        'expiry_date' => 'date',
        'cancelled_at' => 'datetime',
        'worldpay_token' => 'array',
    ];

    public function user()
    {
        return $this->belongsTo(User::class, 'user_id', 'id');
    }
}


Step 3: Create a controller named PaymentController and paste the below code:

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Models\Membership;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class PaymentController extends Controller
{
    public function createPaymentFromSession(Request $request)
    {
        /**
         * Transaction reference format :
         * TXN_1_1234 for free trail
         * SUB_1_1234 for subscription
         */
        $sessionState = $request->session_state;
        $authUserId = auth()->id();
        $user = User::where('id', $authUserId)->first();
        // Check if user has an existing membership, if yes then charge the renewal amount else charge the free trial amount
        $existingMembership = Membership::where('user_id', $authUserId)->first();
        if ($existingMembership) {
            $transactionRef = 'SUB_' . $authUserId . '_' . time();
            $amount = (float) config('app.subscription_amount') ?? 2;
        }else{
            $transactionRef = 'TXN_' . $authUserId . '_' . time();
            $amount = 0;
        }

        $response = Http::withHeaders([
                            'Content-Type' => 'application/json',
                            'WP-Api-Version'       => '2024-06-01',
                            'Authorization' => 'Basic ' . base64_encode(env('WORLDPAY_USERNAME') . ':' . env('WORLDPAY_PASSWORD')),
                        ])->post(env('WORLDPAY_MODE_URL').'/api/payments', [
                            "transactionReference" => $transactionRef,
                            "merchant" => [
                                "entity" => env('WORLDPAY_ENTITY'),
                            ],
                            "instruction" => [
                                "method" => "card",
                                "paymentInstrument" => [
                                    "type" => "checkout",
                                    "cardHolderName" => $user->name,
                                    "sessionHref" => $sessionState,
                                ],
                                "tokenCreation" => [
                                    "type" => "worldpay"
                                ],
                                "customerAgreement" => [
                                    "type" => "subscription",
                                    "storedCardUsage" => "first"
                                ],
                                "narrative" => [
                                    "line1" => "Members Discounts Ltd",
                                ],
                                "value" => [
                                    "currency" => "GBP",
                                    "amount" => $amount // keep 0 for free trial and $request->amount for subscription
                                ]
                            ]
                        ]);

        $sessionResponse = $response->json();
        // Log::info('Worldpay createPaymentFromSession Response:', $sessionResponse);

        if (isset($sessionResponse['errorName'])) {
            Log::info('Worldpay Error : ', $sessionResponse);

            return response()->json([
                'message' => $sessionResponse['message'] ?? 'Worldpay error',
                'error' => $sessionResponse['errorName'],
            ], 400);
        }

        if ($sessionResponse['outcome'] !== 'authorized') {
            Log::info('Worldpay Payment authorization failed : ', $sessionResponse);

            $message = isset($sessionResponse['message']) ? $sessionResponse['message'] : 'Payment authorization failed';
            return response()->json([
                'message' => $message,
                'paymentId' => $data['paymentId'] ?? null,
                'status' => $sessionResponse['outcome']
            ], 400);
        }

        $trailDay = (int) env('TRAIL_DAYS', 7);
        $joinedDate = Carbon::now()->toDateString();
        $expiryDate = Carbon::now()->addDays($trailDay)->toDateString();

        if($existingMembership){
            $existingMembership->update([
                'expiry_date' => $expiryDate,
                'is_free_trail_completed' => true,
                'status' => 'active',
                'worldpay_token' => $response->json(),
            ]);
            return response()->json([
                'message' => 'Payment authorized successfully',
                'status' => $sessionResponse['outcome'],
            ], 200);
        }

        $newMembershipCreated = Membership::create([
            'user_id' => auth()->id() ?? 2,
            'joined_date' => $joinedDate,
            'expiry_date' => $expiryDate,
            'is_free_trail_completed' => false,
            'status' => 'free_trail_active',
            'worldpay_token' => $response->json(), // save token for later auto-renewal
        ]);

        if(!$newMembershipCreated){
            Log::info('Worldpay Payment authorized BUT Membber ship not created : ', $sessionResponse);

            return response()->json([
                'message' => 'Payment authorized successfully. but Membership not created',
                'paymentId' => $data['paymentId'] ?? null,
                'status' => $sessionResponse['outcome']
            ], 500);
        }

        return response()->json([
            'message' => 'Payment authorized successfully',
            'status' => $sessionResponse['outcome'],
        ], 200);
    }
}


Your Subscription for 7 days is paid and your card details is stored safely with Worldpay using a session and token. 

Now, you have to take the future payments automatically (recurring payments - subscriptions)

Step 4: Open a terminal and run the below command to create a command file:

php artisan make:command ChargeSubscriptions

This will create a file inside: app/Console/Commands/ChargeSubscriptions.php 

Step 5: Go to routes/console.php file and paste the below code.

/** This will charge subscriptions after the subscription expires. if it fails, it sets the status to expired */
Schedule::command('subscriptions:charge')->daily();


Step 6: Open the file app/Console/Commands/ChargeSubscriptions.php and paste the below code:

<?php

namespace App\Console\Commands;

use App\Jobs\ChargeMembershipJob;
use App\Models\Membership;
use Illuminate\Console\Command;

class ChargeSubscriptions extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'subscriptions:charge';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Execute the console command.
     */
    public function handle()
    {
        // \Log::info('Scheduler is running '.now()->toDateString());

        $memberships = Membership::whereIn('status', ['free_trail_active', 'active'])
            ->where('expiry_date', '<=', now()->toDateString())
            ->get();

        // \Log::info('Found '.count($memberships).' memberships to charge');
        foreach ($memberships as $membership) {
            // dispatch(new ChargeMembershipJob($membership));
            ChargeMembershipJob::dispatch($membership);
        }
    }
}


Step 7: Generate the Job Class. open a terminal and run the below command:

php artisan make:job ChargeMembershipJob

This will create a file inside: app\Jobs\ChargeMembershipJob.php

Step 8: Open the file ChargeMembershipJob.php and paste the below code:

<?php

namespace App\Jobs;

use App\Models\Setting;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Http;

class ChargeMembershipJob implements ShouldQueue
{
    use Queueable;

    protected $membership;

    public function __construct($membership)
    {
        $this->membership = $membership;
    }

    public function handle()
    {
        $membership = $this->membership;
        // \Log::info('ChargeMembershipJob handle called', [
        //             'membership' => $membership,
        //         ]);
        if (!$membership->worldpay_token) {
            $membership->update([
                'status' => 'expired',
            ]);
            return;
        }
        $this->chargeToken($membership);

    }

    public function chargeToken($membership)
    {
        $subscriptionAmount = (float) config('app.subscription_amount') ?? 4.99;

        // \Log::info('Recurring Charge REQUEST called', [
        //     'membership' => $membership,
        // ]);

        /**
         * Do not change the transaction reference format. This is used for other worldpay related operations.
         * Eg:- $transactionRef = 'SUB_' . $user->id . '_' . time();
         * Documentation url : https://docs.worldpay.com/access/products/payments/recurring-first
         * Refer the checkout SDK on the documentation
         */
        $transactionRef = 'SUB_' . $membership->user_id . '_' . time();
        $renewalAmount = $subscriptionAmount;

        $tokenData = is_array($membership->worldpay_token) ? $membership->worldpay_token : json_decode($membership->worldpay_token, true);

        $tokenHref = $tokenData['token']['href'] ?? ($tokenData['tokenPaymentInstrument']['href'] ?? null);
        $schemeReference = $tokenData['schemeReference'] ?? null;

        $response = Http::withBasicAuth(env('WORLDPAY_USERNAME'), env('WORLDPAY_PASSWORD'))
                    ->withHeaders([
                        'Content-Type' => 'application/vnd.worldpay.payments-v7+json',
                        'Accept' => 'application/vnd.worldpay.payments-v7+json'
                    ])->post(env('WORLDPAY_MODE_URL').'/cardPayments/merchantInitiatedTransactions',[
                        "transactionReference" => $transactionRef,
                        "merchant" => [
                            "entity" => env('WORLDPAY_ENTITY')
                        ],
                        "instruction" => [
                            "requestAutoSettlement" => [
                                "enabled" => true
                            ],
                            "narrative" => [
                                "line1" => "Members Discounts",
                            ],
                            "value" => [
                                "currency" => "GBP",
                                "amount"   => $renewalAmount,
                            ],
                            "paymentInstrument" => [
                                "type" => "card/token",
                                "href" => $tokenHref,
                            ],
                            "customerAgreement" => [
                                "type" => "subscription",
                                "storedCardUsage" => "subsequent",
                                "schemeReference" => $schemeReference
                            ],
                        ],
                        // "channel" => "moto" // merchant-initiated
                    ]);

        // \Log::info('Recurring Charge Response ALONE', [
        //     'response' => $response->json()
        // ]);
        // \Log::info('Recurring Charge Response', [
        //     'membership' => $membership,
        //     'membership_id' => $membership->id,
        //     'status' => $response->status(),
        //     'body' => $response->body()
        // ]);

        return $response;
    }
}


Now you have to update the Webhook URL on your Worldpay dashboard to know the Payment was done successfully or not on the Backend. 

The URL will be like: https://yourdomain.com/api/worldpay/webhook

Step 9: Create a route as given below:

Route::post('/worldpay/webhook', [MembershipController::class, 'webhook']);
Route::delete('/membership-cancel', [MembershipController::class, 'cancelMembership']);


Step 10: Create a controller named MembershipController.php and paste the below code:

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Models\Membership;
use App\Models\Payment;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

class MembershipController extends Controller
{
    public function cancelMembership(Request $request)
    {
        $authUserId = auth()->id();
        $todayDate = Carbon::now()->toDateString();

        $membership = Membership::where('user_id', $authUserId)
                        ->whereIn('status', ['active','free_trail_active'])
                        ->where('expiry_date', '>=', $todayDate)
                        ->first();
        if($membership){
            // Call the payment gateway to cancel the membership

            $tokenData = is_array($membership->worldpay_token) ? $membership->worldpay_token : json_decode($membership->worldpay_token, true);
            $tokenHref = $tokenData['tokenPaymentInstrument']['href'] ?? null;

            $response = Http::withBasicAuth(env('WORLDPAY_USERNAME'), env('WORLDPAY_PASSWORD'))
                            ->delete($tokenHref);

            // \Log::warning('delete token response:');

            // if a user cancels the membership before the free trial period is over, we will still update his free trial as completed
            $membership->update([
                'status' => 'cancelled',
                'is_free_trail_completed' => true,
                'cancelled_at' => Carbon::now()->toString(),
                'worldpay_token' => null,
            ]);

            return response()->json([
                'status' => 200,
                'message' => 'You have successfully cancelled your membership'
            ]);
        }

        return response()->json([
            'status' => 500,
            'message' => 'You do not have an active membership'
        ], 404);
    }

    public function webhook(Request $request)
    {
        $data = $request->all();

        \Log::info('Worldpay Webhook', $data);

        // Normalize event type
        $eventType = data_get($data, 'eventType')
                ?? data_get($data, 'eventDetails.type');

        // ============================
        // PAYMENT AUTHORIZED EVENT
        // ============================
        if ($eventType === 'authorized') {

            $transactionRef = data_get($data, 'eventDetails.transactionReference');

            $amount = data_get($data, 'eventDetails.amount.value');
            $currency = data_get($data, 'eventDetails.amount.currencyCode');

            $userId = $this->getUserFromTxn($transactionRef);

            $user = User::where('id',$userId)->first();
            if (!$user) {
                \Log::error('Authorized: Missing or invalid user', $data);
                return response()->json(['status' => 'error'], 200);
            }

            // Handle recurring vs first payment
            if (str_starts_with($transactionRef, 'SUB_')) {

                $membershipUserId = explode('_', $transactionRef)[1];

                $membership = Membership::where('user_id', $membershipUserId)->first();

                if ($membership) {
                    $membership->update([
                        'expiry_date' => now()->addYear(),
                        'is_free_trail_completed' => true,
                        'status' => 'active'
                    ]);
                }

            } else {

                // First successful payment (if you ever charge upfront)
                $membership = Membership::where('user_id', $userId)->latest()->first();
            }

            // Prevent duplicate payments
            if (Payment::where('transaction_number', $transactionRef)->exists()) {
                return response()->json(['status' => 'duplicate'], 200);
            }

            // Create Payment
            Payment::create([
                'user_id'            => $userId,
                'membership_id'      => $membership->id ?? null,
                'amount'             => $amount,
                'currency'           => $currency,
                'outcome'            => 'authorized',
                'transaction_number' => $transactionRef,
                'payment_id'         => data_get($data, 'eventId'),
                'payment_mode'       => 'online',
                'status'             => 'paid',
                'raw_response'       => json_encode($data),
            ]);

            return response()->json(['status' => 'payment recorded'], 200);
        }
        if($eventType === 'cancelled' || $eventType === 'error' || $eventType === 'expired' || $eventType === 'refused') {
            $transactionRef = data_get($data, 'eventDetails.transactionReference');
            $userId = $this->getUserFromTxn($transactionRef);

            $membership = Membership::where('user_id', $userId)->first();

            if ($membership) {
                $membership->update([
                    'is_free_trail_completed' => true,
                    'status' => 'expired'
                ]);
            }
        }
        // ============================
        // IGNORE OTHER EVENTS
        // ============================
        return response()->json(['status' => 'ignored'], 200);
    }

    private function getUserFromTxn($txn)
    {
        $parts = explode('_', $txn);
        return $parts[1] ?? null;
    }
}


That's all!

https://www.fundaofwebit.com/post/how-to-integrate-worldpay-payment-gateway-for-subscriptions-in-vue-js-and-nuxt-js

Share this blog on social platforms