Skip to main content

Overview

This guide covers best practices for integrating Terminal into fuel card workflows. It’s organized into key implementation areas that map to the fuel card lifecycle:

What This Guide Covers

This guide walks through the complete fuel card workflow with Terminal. Depending on your use case, some sections may be optional:
SectionDescriptionWhen to Use
Connecting FleetsOnboarding fleets and accessing telematics dataAll implementations—required for telematics access
Connection LifecycleHandling connection events, disconnections, and monitoringAll implementations—critical for maintaining data access
API UsageEntity sync, transaction verification, and polling strategiesAll implementations—core integration pattern
IFTA & ReconciliationAutomating compliance reporting and transaction enrichmentIf you handle IFTA reporting or expense reconciliation
OffboardingArchiving connections when accounts closeAll implementations—for clean lifecycle management
We recommend a phased rollout to de-risk your integration and validate data quality before enabling transaction declines:
PhaseScopeWhat You’re Validating
V0: Logging OnlyLog GPS distance at authorization, no declinesData quality, coverage gaps, false positive rates
V1: Location VerificationEnable transaction declines based on locationThreshold tuning, operational workflows
V2: Fuel VerificationAdd fuel level and tank-capacity checksFueling to data presence delay, noise from sloshing
V3: Full Feature SetRouting, IFTA, analyticsEnd-to-end value delivery
Start with V0: Run in logging-only mode for 2-4 weeks to establish baseline metrics before enabling declines. This lets you tune distance thresholds and identify edge cases (e.g., card-not-present transactions, multi-vehicle fleets) without impacting cardholders.

The first step is obtaining consent from the fleet to access their telematics data. Terminal’s Link component handles the consent flow and authorization with the telematics provider. Use Terminal Link to guide fleets through connecting their telematics provider. For fuel cards, use Automatic sync mode from the start—you need continuous data access for fraud prevention.
The hosted flow is a URL you can send directly to fleets via email, SMS, or any messaging channel:
https://link.withterminal.com/?syncMode=automatic&tags=account-1234,program-premium&key={PUBLISHABLE_KEY}
Use tags to associate connections with your internal identifiers like account numbers (account-1234), fleet IDs (fleet-5678), or program tiers (program-premium).

Connection Lifecycle

Managing connection events is critical for fuel card workflows. A disconnected connection means degraded fraud protection—use webhooks to automate responses to connection state changes.

Handling Connection Completion

When a fleet successfully connects their telematics provider, Terminal sends a connection.completed webhook. Use this to:
  • Enable telematics-backed features for that fleet (Location Verification, routing, etc.)
  • Update your internal systems with the connection details
  • Send a confirmation to the fleet
// Webhook handler for connection.completed
app.post('/webhooks/terminal', async (req, res) => {
  const event = req.body;

  if (event.type === 'connection.completed') {
    const { connection } = event.detail;

    // Find account number from tags (e.g., 'account-1234')
    const accountTag = connection.tags?.find((t) => t.startsWith('account-'));
    const accountNumber = accountTag?.replace('account-', '');

    // Record that fleet has connected telematics
    await recordTelematicsConnected({
      accountNumber,
      connectionId: connection.id,
      provider: connection.provider.name,
      connectedAt: event.timestamp,
    });

    // Enable telematics features for this account
    await enableTelematicsFeatures({
      accountNumber,
      features: ['location_verification', 'fuel_verification'],
    });

    // Notify the fleet
    await sendFleetConfirmation({
      email: getFleetEmail(accountNumber),
      subject: 'Telematics Connected Successfully',
      message:
        'Your telematics data is now connected. Location Verification fraud protection is active.',
    });
  }

  res.status(200).send('OK');
});

Handling Disconnections

When a connection becomes disconnected (credentials expire, provider access revoked, etc.), Terminal sends a connection.disconnected webhook. This can result in a fraud protection gap—notify the fleet promptly and degrade gracefully.
if (event.type === 'connection.disconnected') {
  const { connection } = event.detail;

  // Find account number from tags
  const accountTag = connection.tags?.find((t) => t.startsWith('account-'));
  const accountNumber = accountTag?.replace('account-', '');

  // Flag the account for reduced protection
  await updateAccountStatus({
    accountNumber,
    telematicsStatus: 'disconnected',
    fraudProtectionLevel: 'reduced', // Fall back to static auth rules
  });

  // Send urgent notification to fleet with reconnection link
  await notifyFleet({
    email: getFleetEmail(accountNumber),
    subject: 'Action Required: Reconnect Your Telematics',
    body: 'Your telematics connection has been disconnected. Location Verification fraud protection is temporarily reduced. Please reconnect to restore full protection.',
    reconnectUrl: connection.linkUrl,
    priority: 'high',
  });

  // Alert your operations team
  await alertOpsTeam({
    accountNumber,
    reason: 'telematics_disconnected',
    impact: 'Fraud protection degraded to static rules',
  });
}

Handling Reconnections

When a previously disconnected connection is restored, Terminal sends a connection.reconnected webhook. Resume full telematics-backed controls.
if (event.type === 'connection.reconnected') {
  const { connection } = event.detail;

  // Find account number from tags
  const accountTag = connection.tags?.find((t) => t.startsWith('account-'));
  const accountNumber = accountTag?.replace('account-', '');

  // Restore full protection
  await updateAccountStatus({
    accountNumber,
    telematicsStatus: 'connected',
    fraudProtectionLevel: 'full',
  });

  // Confirm to the fleet
  await notifyFleet({
    email: getFleetEmail(accountNumber),
    subject: 'Telematics Reconnected',
    body: 'Your telematics connection has been restored. Location Verification fraud protection is fully active.',
  });
}

Key Webhook Events for Fuel Cards

EventTriggerRecommended Action
connection.completedFleet completes telematics connectionEnable Location Verification and verification features
connection.disconnectedConnection credentials become invalidDegrade to static auth, notify fleet
connection.reconnectedPreviously disconnected connection restoredRestore full telematics features
See the Webhooks Guide for setup instructions and the Webhook Events Reference for detailed payload schemas.

API Usage

For fuel card implementations, you’ll use Terminal’s API for two key workflows: syncing fleet entities (vehicles, drivers, trailers) and real-time location verification for fraud prevention.

Entity Sync: Vehicles, Drivers, Trailers

When a fleet connects their telematics, sync their roster to auto-populate your card management system. This eliminates manual data entry and keeps your system in sync with the fleet’s actual assets. Start with GET /vehicles, GET /drivers, and GET /trailers.
async function syncFleetRoster(connectionToken: string) {
  // Get vehicles from Terminal
  const vehiclesResponse = await fetch(
    'https://api.withterminal.com/tsp/v1/vehicles',
    {
      headers: {
        Authorization: `Bearer ${SECRET_KEY}`,
        'Connection-Token': connectionToken,
      },
    },
  );

  const vehicles = await vehiclesResponse.json();

  // Auto-populate your card management system
  for (const vehicle of vehicles.results) {
    await createOrUpdateVehicleRecord({
      vin: vehicle.vin,
      name: vehicle.name,
      licensePlate: vehicle.licensePlate,
      fuelType: vehicle.fuelType,
      tankCapacityLiters: vehicle.fuelTankCapacity,
      telematicsId: vehicle.id,
    });
  }

  // Repeat for /drivers and /trailers endpoints
  return {
    vehiclesImported: vehicles.results.length,
  };
}
Keeping entities in sync with webhooks: Subscribe to entity webhooks to stay in sync as the fleet’s roster changes:
// Webhook handler for entity changes
app.post('/webhooks/terminal', async (req, res) => {
  const event = req.body;

  if (event.type === 'vehicle.added') {
    const { vehicle, connection } = event.detail;
    await createOrUpdateVehicleRecord({
      vin: vehicle.vin,
      name: vehicle.name,
      fuelType: vehicle.fuelType,
      tankCapacityLiters: vehicle.fuelTankCapacity,
      telematicsId: vehicle.id,
    });
  }

  if (event.type === 'vehicle.removed') {
    const { vehicle } = event.detail;
    await flagVehicleAsRemoved(vehicle.id);
  }

  if (event.type === 'driver.added') {
    const { driver } = event.detail;
    await createOrUpdateDriverRecord({
      firstName: driver.firstName,
      lastName: driver.lastName,
      licenseNumber: driver.licenseNumber,
      telematicsId: driver.id,
    });
  }

  if (event.type === 'driver.removed') {
    const { driver } = event.detail;
    await flagDriverAsRemoved(driver.id);
  }

  res.status(200).send('OK');
});

Transaction Verification: Location & Fuel

For card authorization, we recommend the fuel card provider poll Terminal on a schedule and maintain a traceable copy of vehicle state (location snapshots, tank capacity, timestamps, provider metadata) in your own datastore. Then at swipe time, evaluate Location Verification and fuel verification (capacity checks) from your local data, while using Terminal API reads only as fallback/refresh paths (see Managed Polling and real-time data). Recommended authorization flow: Key endpoints:
EndpointUse CaseLatency
GET /vehicles/locationsCurrent GPS position for Location VerificationReal-time
GET /vehicles/{id}Tank capacity for fuel verificationCached
GET /ifta/summaryJurisdictional miles for IFTAAggregated

Fuel Verification Implementation

Verify that the requested fuel amount doesn’t exceed the vehicle’s physical capacity.
async function verifyFuelCapacity(
  vehicleId: string,
  connectionToken: string,
  requestedGallons: number,
): Promise<{
  approved: boolean;
  maxGallons: number;
  reason?: string;
}> {
  // Get current vehicle location (includes fuel percentage)
  const locationResponse = await fetch(
    `https://api.withterminal.com/tsp/v1/vehicles/locations?vehicleIds=${vehicleId}`,
    {
      headers: {
        Authorization: `Bearer ${SECRET_KEY}`,
        'Connection-Token': connectionToken,
      },
    },
  );

  const locationData = await locationResponse.json();
  const currentLocation = locationData.results[0];

  // Get vehicle details for tank capacity
  const vehicleResponse = await fetch(
    `https://api.withterminal.com/tsp/v1/vehicles/${vehicleId}`,
    {
      headers: {
        Authorization: `Bearer ${SECRET_KEY}`,
        'Connection-Token': connectionToken,
      },
    },
  );

  const vehicle = await vehicleResponse.json();

  if (!vehicle.fuelTankCapacity) {
    // Tank capacity unknown—approve but flag for review
    return {
      approved: true,
      maxGallons: requestedGallons,
      reason: 'Tank capacity unknown—approved with flag',
    };
  }

  // Convert liters to gallons (Terminal uses liters)
  const tankCapacityGallons = vehicle.fuelTankCapacity * 0.264172;
  const currentFuelPercent = currentLocation?.fuel || 0;
  const currentFuelGallons = tankCapacityGallons * (currentFuelPercent / 100);
  const maxFillableGallons = tankCapacityGallons - currentFuelGallons;

  // Add 10% buffer for measurement variance
  const maxWithBuffer = maxFillableGallons * 1.1;

  if (requestedGallons > maxWithBuffer) {
    return {
      approved: false,
      maxGallons: maxFillableGallons,
      reason: `Requested ${requestedGallons.toFixed(1)} gal exceeds capacity (max: ${maxFillableGallons.toFixed(1)} gal)`,
    };
  }

  return {
    approved: true,
    maxGallons: maxFillableGallons,
  };
}

Polling Strategy for Location Verification

The /vehicles/locations endpoint is real-time—each request triggers a synchronous call to the telematics provider. You have two approaches:
ApproachBest ForTrade-offs
On-demandLocation Verification checks at swipe timeHighest freshness, but 1-3s latency
Managed PollingHigh-volume authorization workflowsSub-100ms latency, but data may be 15-60s old

On-Demand Calls

Fetch data directly from the telematics provider at request time. Best when latency of 1-3 seconds is acceptable and you want maximum data freshness.

Managed Polling

For sub-second response times, enable Managed Polling in your Terminal dashboard. Terminal proactively caches location data at your configured interval (e.g., every 15 seconds).
Latency TargetRecommended ApproachTrade-off
Less than 800msManaged Polling (15s)Location may be 15s old
1-3s acceptableOn-DemandFreshest data, higher latency
For sub-second Location Verification: Most production implementations use managed polling with a 15-30 second interval. A vehicle’s location rarely changes significantly in 15 seconds, making this acceptable for fraud prevention while achieving sub-500ms authorization response times.
Production recommendation: Start with on-demand calls during pilot to validate data quality, then switch to managed polling when you need sub-second latency for production authorization flows.

IFTA & Reconciliation

After transactions occur, use Terminal data to automate IFTA compliance and enrich transactions for reconciliation.

IFTA Automation

The /ifta/summary endpoint provides jurisdictional miles by vehicle and month—the key input for IFTA reporting.
async function getIFTASummary(
  connectionToken: string,
  startMonth: string, // 'YYYY-MM'
  endMonth: string,
): Promise<IFTASummary[]> {
  const response = await fetch(
    `https://api.withterminal.com/tsp/v1/ifta/summary?` +
      `startMonth=${startMonth}&endMonth=${endMonth}&groupBy=vehicle,jurisdiction`,
    {
      headers: {
        Authorization: `Bearer ${SECRET_KEY}`,
        'Connection-Token': connectionToken,
      },
    },
  );

  const data = await response.json();
  return data.results;
}

// Example response:
// [
// { vehicle: 'vcl_123', jurisdiction: 'TX', distance: 1234.5, month: '2024-01' },
// { vehicle: 'vcl_123', jurisdiction: 'OK', distance: 567.8, month: '2024-01' },
// { vehicle: 'vcl_456', jurisdiction: 'TX', distance: 890.1, month: '2024-01' },
// ]

Dashboard Access

The Terminal dashboard provides a visual interface for investigating transactions and verifying vehicle activity without writing code. Fraud and operations teams can:
  • View vehicle history: See trips, GPS breadcrumbs, and routes on an interactive map with timeline filtering
  • Verify transaction locations: Check where a vehicle was at a specific time to validate or investigate fuel purchases
  • Browse live locations: See current fleet positions for real-time verification
  • Filter by date and vehicle: Narrow down to specific vehicles and time ranges around suspicious transactions
  • Export data: Download trip and vehicle data as CSV for analysis or dispute resolution
This is particularly useful for fraud investigation teams who need to quickly verify whether a vehicle was actually at a fuel station when a transaction occurred.

Offboarding

When a fleet churns or a contract ends, archive the connection to stop data syncing and clean up resources.

Archiving Connections

Mark a connection as archived when it’s no longer needed using PATCH /connections/current:
curl --request PATCH \
  --url https://api.withterminal.com/tsp/v1/connections/current \
  --header 'Authorization: Bearer {SECRET_KEY}' \
  --header 'Connection-Token: {CONNECTION_TOKEN}' \
  --header 'Content-Type: application/json' \
  --data '{
    "status": "archived"
  }'
Example: Account Closure Workflow
async function handleAccountClosed(accountNumber: string) {
  // Find the connection by account tag
  const connections = await fetch(
    `https://api.withterminal.com/tsp/v1/connections?tag=account-${accountNumber}`,
    { headers: { Authorization: `Bearer ${SECRET_KEY}` } },
  );

  const data = await connections.json();
  const connection = data.results?.[0];

  if (connection) {
    // Archive the connection
    await fetch('https://api.withterminal.com/tsp/v1/connections/current', {
      method: 'PATCH',
      headers: {
        Authorization: `Bearer ${SECRET_KEY}`,
        'Connection-Token': connection.token,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        status: 'archived',
      }),
    });

    // Record the archival in your system
    await recordConnectionArchived({
      accountNumber,
      connectionId: connection.id,
      reason: 'account_closed',
      archivedAt: new Date().toISOString(),
    });

    // Disable telematics features
    await disableTelematicsFeatures({
      accountNumber,
      features: ['gps_lock', 'fuel_verification'],
    });
  }
}
Archiving a connection stops all data syncing and deliveries. Historical data remains in your systems for audits, disputes, and analytics.

Complete Webhook Reference

Here’s a summary of all webhook events relevant to fuel card workflows:
EventTriggerRecommended Action
connection.completedFleet completes telematics connectionEnable Location Verification and verification; sync fleet roster
connection.disconnectedConnection credentials become invalidDegrade fraud protection, notify fleet
connection.reconnectedPreviously disconnected connection restoredRestore full telematics features
vehicle.addedNew vehicle detected in fleetAuto-add to card system, update mappings
vehicle.removedVehicle removed from fleetFlag/remove from card system
vehicle.modifiedVehicle details changedUpdate tank capacity, VIN records
driver.addedNew driver detected in fleetAuto-add to card system
driver.removedDriver removed from fleetFlag/deactivate driver cards
For event payload details, see the Webhook Events Reference.

Full Sequence Diagram


Next Steps