Skip to main content
In this tutorial, you will learn how to use the GBG GO APIs to build a document upload and verification flow. This tutorial illustrates one of the most common use cases for customer verification and onboarding. By following along, you’ll get started quickly by implementing the GO APIs.

What this tutorial covers

By the end, you’ll have a fully functional demo application that:
  • Let’s you upload ID documents such as a passport and a driver’s license
  • Sends the document to GBG GO APIs for verification
  • Verifies documents
Once verification is complete, you can view journey details and outcomes in the Investigation portal.

Prerequisites

  • For business users: Access to the GO dashboard for adding the documents modules to the journey.
  • For developers:
    • Node.js v18 or higher installed on your local machine.
    • Next.js v16 installed on your local machine.
    • A working knowledge of JavaScript (Node.js) or Python.
    • GBG GO API credentials. For more details on required API credentials, refer to Quickstart for developers.

Get started

To get started with this tutorial:
  1. Add the documents modules to the journey, as shown in the image below:
Document modules in journey builder
For instructions on how to add modules to a journey, refer to Configure modules.
  1. Click Publish to Preview.
Modules come with default outcomes, which is used in this tutorial. You can configure different outcomes for each module. To learn more about configuring outcomes, refer to How to configure module outcomes in GO.
  1. Navigate to Dashboard in the journey builder to get your resource ID and version for integrating the journey with the API and app.
Next, set up the development environment for integration.

Set up the development environment for integration

This section provides steps on how to set up your developer environment for this tutorial. Follow the steps below to set up environment:

Step 1: Create a new folder for your project

Open your terminal or command prompt and run:
BASH
mkdir gbg-document-demo 
This command works for both Windows, Mac, and Linux systems.
This creates a new folder in your directory. Then run the command below to navigate into the newly created folder:
BASH
cd gbg-document-demo 
Next, open the folder in your code editor.

Step 2: Initialise the Next.js project

To initialise a Next.js project, run the command below in your terminal:
BASH
npx create-next-app@latest . --typescript --tailwind --app --no-src-dir --import-alias "@/*" 
Next, run the command below to install Axios for cleaner error handling and automatic request and response transformations:
BASH
npm install axios
After installation is completed, create a .env.local file in your root directory to store the environment variables for your API credentials and base URL:
variables
GBG_CLIENT_ID=your_client_id_here
GBG_CLIENT_SECRET=your_client_secret_here
GBG_USERNAME=your_username_here
GBG_PASSWORD=your_password_here
GBG_JOURNEY_ID=your_resource_id@version
GBG_API_BASE_URL=api_base_url
GBG_AUTH_URL=auth_url_endpoint
Replace the placeholder values with your actual GBG GO API credentials and journey details. Refer the Quickstart for developers guide for more information on obtaining these credentials.

Set up API routes for backend code

Next.js API routes run on the server, acting as a secure proxy between your frontend and GBG GO API. In this section, you’ll create routes to handle various logic for your backend app.

Create authentication route

This route handles the API authentication logic for your app. To get started, go to your terminal and create a new folder and file using the command:
BASH
mkdir -p app/api/auth 
Then, create a file under the auth folder called route.ts and paste in the code below:
typecript
// app/api/auth/route.ts
import { NextResponse } from 'next/server';
import axios from 'axios';

export async function POST() {
  try {
    const response = await axios.post(
      process.env.GBG_AUTH_URL!,
      new URLSearchParams({
        grant_type: 'password',
        client_id: process.env.GBG_CLIENT_ID!,
        client_secret: process.env.GBG_CLIENT_SECRET!,
        username: process.env.GBG_USERNAME!,
        password: process.env.GBG_PASSWORD!,
      }),
      {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
      }
    );

    return NextResponse.json({
      success: true,
      token: response.data.access_token,
      expiresIn: response.data.expires_in,
    });
  } catch (error: any) {
    console.error('Authentication error:', error.response?.data || error.message);
    return NextResponse.json(
      {
        success: false,
        error: 'Failed to authenticate with GBG GO API',
        details: error.response?.data?.error_description || error.message,
      },
      { status: 401 }
    );
  }
}
What this route does:
  • Exchanges your credentials for a temporary access token.
  • Uses OAuth2 password grant flow, which is standard for API authentication.
  • Returns the token to the frontend for subsequent requests.
  • Handles errors with detailed messages.
Why separate authentication? Tokens expire after an hour. By separating this logic, you can implement token refresh later without changing your upload flow.

Create upload and start journey route

In this section, you’ll create the upload and start the journey route. This handles the logic for starting the journey. To get started, go to your terminal and create a new folder and file using the command:
BASH
mkdir -p app/api/journey/upload 
Then, create a file under the upload folder called route.ts and paste in the code below:
typescript
// app/api/journey/upload/route.ts
import { NextResponse } from 'next/server';
import axios from 'axios';

export async function POST(request: Request) {
  try {
    const formData = await request.formData();
    const token = formData.get('token') as string;
    const file = formData.get('file') as File;

    if (!file) {
      return NextResponse.json(
        { success: false, error: 'No file provided' },
        { status: 400 }
      );
    }

    // Convert file to base64
    const bytes = await file.arrayBuffer();
    const buffer = Buffer.from(bytes);
    const base64Image = buffer.toString('base64');

    // Start journey in PREFILL MODE with document
    const response = await axios.post(
      `${process.env.GBG_API_BASE_URL}/captain/api/journey/start`,
      {
        resourceId: process.env.GBG_JOURNEY_ID,
        context: {
          subject: {
            identity: {
              firstName: 'Demo',
              lastName: 'User',
            },
            documents: [
              {
                side1Image: base64Image, // Document goes here!
              },
            ],
            biometrics: [],
          },
        },
      },
      {
        headers: {
          Authorization: `Bearer ${token}`,
          'Content-Type': 'application/json',
        },
      }
    );

    console.log('Full upload response:', JSON.stringify(response.data, null, 2));

    // Extract instanceId - could be at different paths
    const instanceId = response.data.instanceId || response.data.resourceId || response.data.id || response.data.journeyInstanceId;

    if (!instanceId) {
      console.error('No instanceId found in response. Full data:', response.data);
      return NextResponse.json(
        {
          success: false,
          error: 'Failed to get journey instance ID from API response',
          details: response.data
        },
        { status: 500 }
      );
    }

    return NextResponse.json({
      success: true,
      instanceId: instanceId,
      data: response.data,
    });
  } catch (error: any) {
    console.error('Upload error:', error.response?.data || error.message);
    return NextResponse.json(
      {
        success: false,
        error: 'Failed to upload document',
        details: error.response?.data || error.message,
      },
      { status: 500 }
    );
  }
}
Let’s break down what the code above does:
  • File Processing: Converts the uploaded file to base64 encoding.
  • Prefill Context: Includes the document directly in the journey start request under context.subject.documents[].
  • Instance ID: Returns a unique identifier to track this verification session.

Create check status route

In this section, you’ll create a check status route that contains the logic for checking if verification is complete. To get started, go to your terminal and create a new folder and file using the command:
BASH
mkdir -p app/api/journey/status 
Then, create a file under the status folder called route.ts and paste in the code below:
typescript
// app/api/journey/status/route.ts
import { NextResponse } from 'next/server';
import axios from 'axios';

export async function POST(request: Request) {
  try {
    const { token, instanceId } = await request.json();

    console.log('Status check - instanceId:', instanceId);

    if (!instanceId) {
      return NextResponse.json(
        { success: false, error: 'No instanceId provided' },
        { status: 400 }
      );
    }

    const url = `${process.env.GBG_API_BASE_URL}/captain/api/journey/state/fetch`;
    console.log('Status check URL:', url);
    console.log('Status check payload:', { instanceId });

    const response = await axios.post(
      url,
      {
        instanceId: instanceId, // API expects instanceId, not resourceId
      },
      {
        headers: {
          Authorization: `Bearer ${token}`,
          'Content-Type': 'application/json',
        },
      }
    );

    console.log('Status response:', JSON.stringify(response.data, null, 2));

    return NextResponse.json({
      success: true,
      state: response.data,
    });
  } catch (error: any) {
    console.error('Status check error:', error.response?.data || error.message);
    return NextResponse.json(
      {
        success: false,
        error: 'Failed to check status',
        details: error.response?.data || error.message,
      },
      { status: 500 }
    );
  }
}
What this route does:
  • Polls the journey state to check if verification is complete.
  • Returns the current status, for example, InProgress or complete.
  • Includes extracted data once processing is finished.

Build the frontend

Now, you’ll build the user interface that ties everything together. To get started, navigate to app/page.tsx and replace all the content with the code below:
typescript
// app/page.tsx
'use client';

import { useState } from 'react';

export default function Home() {
  const [file, setFile] = useState<File | null>(null);
  const [preview, setPreview] = useState<string | null>(null);
  const [status, setStatus] = useState<string>('');
  const [loading, setLoading] = useState(false);
  const [result, setResult] = useState<any>(null);
  const [error, setError] = useState<string | null>(null);

  // Handle file selection
  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const selectedFile = e.target.files?.[0];

    if (!selectedFile) return;

    // Validate file type
    const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/bmp', 'application/pdf'];
    if (!validTypes.includes(selectedFile.type)) {
      setError('Please upload a valid file (JPEG, PNG, BMP, or PDF). Note: HEIC is not supported.');
      return;
    }

    // Validate file size (max 10MB)
    const maxSize = 10 * 1024 * 1024; // 10MB in bytes
    if (selectedFile.size > maxSize) {
      setError('File size must be less than 10MB');
      return;
    }

    setFile(selectedFile);
    setError(null);

    // Create preview (only for images, not PDF)
    if (selectedFile.type.startsWith('image/')) {
      const reader = new FileReader();
      reader.onloadend = () => {
        setPreview(reader.result as string);
      };
      reader.readAsDataURL(selectedFile);
    } else {
      setPreview(null); // PDF, no preview
    }
  };

  // Handle document upload and verification
  const handleUpload = async () => {
    if (!file) {
      setError('Please select a file first');
      return;
    }

    setLoading(true);
    setError(null);
    setResult(null);
    setStatus('Authenticating...');

    try {
      // Step 1: Get access token
      setStatus('Step 1/3: Getting access token...');
      const authResponse = await fetch('/api/auth', {
        method: 'POST',
      });
      const authData = await authResponse.json();

      if (!authData.success) {
        throw new Error(authData.error || 'Authentication failed');
      }

      const token = authData.token;

      // Step 2: Upload document and start journey (PREFILL MODE)
      setStatus('Step 2/3: Uploading document and starting verification...');
      const uploadFormData = new FormData();
      uploadFormData.append('token', token);
      uploadFormData.append('file', file);

      const uploadResponse = await fetch('/api/journey/upload', {
        method: 'POST',
        body: uploadFormData,
      });
      const uploadData = await uploadResponse.json();

      if (!uploadData.success) {
        throw new Error(uploadData.error || 'Failed to upload document');
      }

      const instanceId = uploadData.instanceId;

      // Step 3: Poll for results
      setStatus('Step 3/3: Processing verification...');

      let attempts = 0;
      const maxAttempts = 15;

      const pollStatus = async (): Promise<void> => {
        attempts++;

        const statusResponse = await fetch('/api/journey/status', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ token, instanceId }),
        });
        const statusData = await statusResponse.json();

        if (!statusData.success) {
          throw new Error(statusData.error || 'Failed to check status');
        }

        const journeyState = statusData.state;
        console.log('Journey state:', journeyState);
        console.log('Status:', journeyState.status);

        // Check if journey is complete
        if (journeyState.status === 'Completed' || journeyState.status === 'COMPLETE' || journeyState.status === 'Complete' || journeyState.outcome) {
          setStatus('Document verification complete');
          setResult(journeyState);
          setLoading(false);
          return;
        }

        // Continue polling if not complete
        if (attempts < maxAttempts) {
          setStatus(`Step 3/3: Processing verification (${attempts}/${maxAttempts})... [Status: ${journeyState.status}]`);
          setTimeout(pollStatus, 2000); // Wait 2 seconds
        } else {
          setStatus('Verification is taking longer than expected.');
          setResult(journeyState); // Show the result even if not complete
          setLoading(false);
        }
      };

      await pollStatus();

    } catch (err: any) {
      setError(err.message || 'An error occurred during verification');
      setStatus('');
      setLoading(false);
    }
  };

  return (
    <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 py-12 px-4">
      <div className="max-w-2xl mx-auto">
        {/* Header */}
        <div className="text-center mb-8">
          <h1 className="text-4xl font-bold text-gray-900 mb-2">
            Document Verification Demo
          </h1>
          <p className="text-gray-600">
            Upload an ID document to verify identity using GBG GO API (Prefill Mode)
          </p>
        </div>

        {/* Main Card */}
        <div className="bg-white rounded-lg shadow-xl p-8">

          {/* File Upload Section */}
          <div className="mb-6">
            <label className="block text-sm font-medium text-gray-700 mb-2">
              Upload ID Document
            </label>
            <input
              type="file"
              accept="image/*,.pdf"
              onChange={handleFileChange}
              disabled={loading}
              className="block w-full text-sm text-gray-500
                file:mr-4 file:py-2 file:px-4
                file:rounded-full file:border-0
                file:text-sm file:font-semibold
                file:bg-indigo-50 file:text-indigo-700
                hover:file:bg-indigo-100
                disabled:opacity-50 disabled:cursor-not-allowed"
            />
            <p className="mt-2 text-xs text-gray-500">
              Supported: JPEG, PNG, BMP, PDFMax size: 10MBHEIC not supported
            </p>
          </div>

          {/* Preview Section */}
          {preview && (
            <div className="mb-6">
              <label className="block text-sm font-medium text-gray-700 mb-2">
                Preview
              </label>
              <div className="border-2 border-dashed border-gray-300 rounded-lg p-4">
                <img
                  src={preview}
                  alt="Document preview"
                  className="max-w-full h-auto mx-auto max-h-96 rounded"
                />
              </div>
            </div>
          )}

          {file && !preview && (
            <div className="mb-6">
              <div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
                <p className="text-sm text-gray-700">
                  📄 {file.name} ({(file.size / 1024).toFixed(2)} KB)
                </p>
              </div>
            </div>
          )}

          {/* Error Message */}
          {error && (
            <div className="mb-6 bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
              <p className="font-medium">Error</p>
              <p className="text-sm">{error}</p>
            </div>
          )}

          {/* Status Message */}
          {status && (
            <div className="mb-6 bg-blue-50 border border-blue-200 text-blue-700 px-4 py-3 rounded">
              <p className="text-sm">{status}</p>
            </div>
          )}

          {/* Upload Button */}
          <button
            onClick={handleUpload}
            disabled={!file || loading}
            className="w-full bg-indigo-600 text-white py-3 px-6 rounded-lg font-semibold
              hover:bg-indigo-700 disabled:bg-gray-300 disabled:cursor-not-allowed
              transition-colors duration-200"
          >
            {loading ? 'Processing...' : 'Verify Document'}
          </button>

          {/* Results Section */}
          {result && (
            <div className="mt-8 border-t pt-6">
              <h2 className="text-xl font-semibold text-gray-900 mb-4">
                Verification Results
              </h2>

              {/* Outcome */}
              <div className="mb-4 p-4 bg-gray-50 rounded-lg">
                <p className="text-sm font-medium text-gray-700 mb-1">Outcome</p>
                <p className="text-lg font-bold text-indigo-600">
                  {result.outcome || 'Processing'}
                </p>
              </div>

              {/* Status */}
              <div className="mb-4 p-4 bg-gray-50 rounded-lg">
                <p className="text-sm font-medium text-gray-700 mb-1">Status</p>
                <p className="text-lg">{result.status}</p>
              </div>

              {/* Extracted Data */}
              {result.context?.subject && (
                <div className="mb-4 p-4 bg-gray-50 rounded-lg">
                  <p className="text-sm font-medium text-gray-700 mb-2">
                    Extracted Information
                  </p>
                  <pre className="text-xs bg-white p-3 rounded border overflow-auto max-h-64">
                    {JSON.stringify(result.context.subject, null, 2)}
                  </pre>
                </div>
              )}

              {/* Full Response */}
              <details className="mt-4">
                <summary className="cursor-pointer text-sm font-medium text-gray-700 hover:text-gray-900">
                  View Full Response
                </summary>
                <pre className="mt-2 text-xs bg-gray-50 p-4 rounded border overflow-auto max-h-96">
                  {JSON.stringify(result, null, 2)}
                </pre>
              </details>
            </div>
          )}
        </div>

        {/* Info Footer */}
        <div className="mt-6 text-center text-sm text-gray-600">
          <p>This demo uses GBG GO API in prefill mode</p>
          <p className="mt-1">Never upload real documents in a test environment</p>
        </div>
      </div>
    </div>
  );
}
Here, React hooks are used to track the file, loading state, errors and verification results. The code also checks for file types and size before allowing upload. For this demo, you can see the processes of authentication, uploading the file, and polling results. Run the command below to start your development server:
BASH
npm run dev
Navigate to localhost:3000, you should see the application running. This is what it should look like:
Document modules in journey builder
Congratulations, You have successfully built a document upload and verification flow using GBG GO APIs and document modules.

Test your demo app

To test your demo app:
  1. Navigate to localhost:3000.
  2. Click Choose File to upload a document, for example, a passport.
Document modules in journey builder
  1. Click Verify Document. The verification process starts running. What happens is that the journey starts, and the document modules begin verifying the document in the background.
You should get a similar response in your terminal like this:
Document modules in journey builder
The response contains comprehensive information about the uploaded document. Details include document type and extracted fields such as FirstName, LastName, and BirthDate. After verification is complete, you should see the “Document verification complete” response in the demo app as shown below:
Document modules in journey builder
Now, you can investigate the details of this session in the Investigation portal as shown below:
Document modules in journey builder
The investigation portal provides a detailed intuitive view of the verification process, including outcomes from each document module. At the top of the page, you’ll notice that the session ID PiJDO... matches the instancedID generated in the terminal. Click the Processing tab to see module outcomes.
Document modules in journey builder
These are the outcomes from the document modules added to the journey. You can click into each module to see more details about the verification results. To do this click More Details. To learn more about the common tabs found in More Details, refer to View details of a customer session.

Additional resources