Skip to content
Telematics & Integration

Samsara, Geotab and Motive Platform Adapters

Three telematics vendors dominate North American fleets, and no two of them speak the same protocol. Samsara authenticates with a bearer token and pushes webhooks; Geotab authenticates through a session-based MyGeotab API and expects you to poll; Motive uses OAuth and its own vehicle-id scheme. If your compliance pipeline hard-codes any one of these, every new customer on a different platform forces a rewrite of the delivery layer — and every vendor rate-limit change becomes a production incident. The fix is an adapter layer that hides each vendor’s auth, rate limits, id scheme, and payload shape behind one internal interface, so the code that emits a DvirComplianceEvent never learns which platform receives it. This guide specifies that layer.

It sits inside the Telematics, ELD & Maintenance Platform Integration section, which defines the canonical DvirComplianceEvent this layer delivers, and it is the vendor-abstraction counterpart to Webhook Event Delivery for DVIR Pipelines, which owns the delivery mechanics. The same adapters serve the ELD Identity and HOS Record Linkage work when it queries each vendor’s driver directory. The concrete base-class and registry implementation is walked through in Building a Vendor-Neutral Telematics Adapter Layer.

A vendor-neutral adapter registry routing one internal event to three telematics APIs A left-to-right dispatch diagram. One internal DvirComplianceEvent enters an adapter registry keyed by a vendor enum. The registry resolves the correct concrete adapter and fans the event out to three per-vendor adapters: a Samsara adapter using bearer-token auth and webhooks, a Geotab adapter using session auth and polling, and a Motive adapter using OAuth. Each concrete adapter normalizes auth, id mapping, and payload shape and calls its vendor API. All three satisfy one shared TelematicsAdapter protocol so the dispatcher speaks a single protocol regardless of vendor. ONE INTERNAL PROTOCOL · TelematicsAdapter DvirComplianceEvent internal canonical event vendor-agnostic Adapter registry keyed by vendor enum resolve(vendor) Samsara adapter bearer token · webhook Geotab adapter session auth · poll Motive adapter OAuth · webhook Samsara API Geotab API Motive API

Prerequisites and Environment Setup

Anchor link to "Prerequisites and Environment Setup"

The adapter layer is a library the delivery service depends on, not a service itself. Target Python 3.10+ (the code uses Protocol, match/case, and X | Y unions) with:

  • Pydantic 2.x — validate per-vendor config and the internal event before it is translated.
  • httpx — one async client per adapter, each with its own timeout, retry budget, and rate-limit policy.
  • typing.Protocol and abc — the structural contract every concrete adapter satisfies.
  • A secrets source — vendor credentials injected as config, never hard-coded; apply the handling from Encrypting DVIR Data at Rest and in Transit.
  • The canonical event — the DvirComplianceEvent defined by the Telematics, ELD & Maintenance Platform Integration section is the single input every adapter accepts.

The one rule that makes the layer worth building: nothing above the adapter may reference a vendor by name. The dispatcher selects an adapter by enum; it never branches on if vendor == "samsara".

The three vendors differ on exactly the axes an adapter must absorb. Knowing where they diverge tells you what the internal protocol has to hide.

Concern Samsara Geotab (MyGeotab) Motive
Auth style Bearer API token Session credential (database + user) OAuth 2.0 access token
Vehicle id scheme Numeric asset id Id GUID-like string Numeric vehicle id
Delivery model Outbound webhooks Poll Get/GetFeed Webhooks + REST
Rate limits Per-token request budget Per-database call budget Per-app request budget
Payload shape Flat JSON Entity objects with typed refs Nested JSON envelopes

Two consequences fall out of this table. First, resolve_vehicle_id must exist as a first-class adapter method, because a vehicle_vin on the internal event maps to a different native id on every platform. Second, delivery cannot assume push: a Geotab adapter’s push_event may enqueue for the next poll cycle rather than fire immediately, so the acknowledgement semantics must be part of the contract rather than assumed synchronous.

The auth axis is the one most likely to leak upward if the boundary is drawn carelessly. A Samsara bearer token is effectively static and needs only presence validation; a Geotab session must be acquired and can expire mid-batch, forcing a re-authentication path; a Motive OAuth token must be refreshed before expiry and carries a refresh token that must be stored as carefully as the access token itself. If the delivery layer had to know which of these it was dealing with, every credential-rotation policy change would ripple through the dispatcher. The adapter absorbs all three behind one authenticate method whose only contract is “after this returns, the adapter can talk to its vendor” — idempotent, so calling it before every batch is safe, and internally responsible for whatever refresh the vendor demands. That single guarantee is what lets the dispatcher call authenticate unconditionally without knowing whether it triggered a no-op, a session fetch, or a token refresh.

The adapter layer standardizes two things: the config each adapter is built from, and the acknowledgement each returns. The event itself is the section’s canonical shape and is never redefined here.

Field Type Enumeration / range Compliance tag
vendor str (enum) samsara, geotab, motive Registry routing key
api_base_url str vendor endpoint Deployment config
credential SecretStr token / session / OAuth Never logged
rate_limit_per_min int vendor-specific budget Backpressure control
ack_mode str (enum) sync, deferred Delivery semantics

Model the config so an adapter cannot be constructed without the fields it needs, and so a credential never lands in a log line:

python
from enum import Enum

from pydantic import BaseModel, SecretStr


class Vendor(str, Enum):
    SAMSARA = "samsara"
    GEOTAB = "geotab"
    MOTIVE = "motive"


class AdapterConfig(BaseModel):
    vendor: Vendor
    api_base_url: str
    credential: SecretStr           # excluded from repr and logs by SecretStr
    rate_limit_per_min: int
    ack_mode: str = "sync"          # "sync" or "deferred" (poll-based vendors)


class DeliveryAck(BaseModel):
    vendor: Vendor
    accepted: bool
    vendor_ref: str | None = None   # vendor-side id for the delivered event
    deferred: bool = False          # True when queued for a later poll cycle

The layer has one abstract protocol, one registry, and one concrete class per vendor. The dispatcher resolves and delegates; it never knows the vendor.

  1. Define the protocol. Declare TelematicsAdapter with authenticate, resolve_vehicle_id, push_event, and an acknowledgement contract. Every concrete adapter must satisfy it structurally.
  2. Implement per-vendor concretes. Each of SamsaraAdapter, GeotabAdapter, and MotiveAdapter translates the internal event into the vendor’s auth, id scheme, and payload shape.
  3. Register by enum. A registry maps each Vendor value to its adapter factory, built from injected config.
  4. Resolve and delegate. The dispatcher looks up the adapter for the fleet’s vendor, resolves the native vehicle id, and calls push_event, returning a normalized DeliveryAck.
  5. Fail an unknown vendor loudly. Resolving a vendor with no registered adapter raises, rather than silently dropping the event.

The protocol is the contract that lets the dispatcher stay vendor-blind:

python
from typing import Protocol, runtime_checkable


@runtime_checkable
class TelematicsAdapter(Protocol):
    vendor: Vendor

    async def authenticate(self) -> None:
        """Acquire or refresh vendor credentials. Idempotent."""
        ...

    async def resolve_vehicle_id(self, vehicle_vin: str) -> str:
        """Map the internal VIN to the vendor's native vehicle id."""
        ...

    async def push_event(self, event: dict) -> DeliveryAck:
        """Translate and deliver a DvirComplianceEvent; return a normalized ack."""
        ...

Compliance Thresholding and Routing

Anchor link to "Compliance Thresholding and Routing"

The adapter layer is deliberately thin on compliance logic — the severity band and compliance_action are already decided by the time an event reaches it — but it carries two obligations that are compliance-relevant, not merely operational:

  • Preserve the event verbatim. An adapter translates the envelope, not the payload’s meaning. The severity_score, compliance_action, and audit_hash must survive translation unchanged, so a 70-and-above OOS event still reads as OOS on the vendor side. Verify the audit_hash before and reject a translation that would alter a compliance-bearing field.
  • Never drop an OOS event to a rate limit. A per-vendor rate limit is real backpressure, but an out-of-service notification cannot simply be discarded when the budget is exhausted. Queue it with priority and deliver on the next available token; treat a silently dropped OOS event as a compliance incident. The delivery guarantees for this live in Idempotent Webhook Retry and Signature Verification.

Because the same DvirComplianceEvent reaches every vendor unchanged, a downstream consumer such as Syncing DVIR Severity Scores to Samsara and Geotab via Webhook sees a consistent compliance signal no matter which platform delivered it.

A subtle but important discipline: the adapter must not enrich or interpret the event on the way out. It is tempting to have a vendor adapter compute a display string, re-derive a severity band, or translate compliance_action into a vendor-specific status label — but every such transformation is a place where the compliance meaning can drift between platforms, and a drifted meaning is exactly what an auditor will catch. Keep the adapter to envelope translation: map the vehicle id, format the payload the vendor’s API demands, attach auth, and carry the compliance-bearing fields through verbatim. If a vendor genuinely needs a derived field, derive it once upstream where the audit_hash still covers it, not inside the adapter where the derivation is invisible to the hash.

Production Integration and Platform Synchronization

Anchor link to "Production Integration and Platform Synchronization"
  • Dependency-inject config; never hard-code a vendor. Build each adapter from an AdapterConfig so credentials and endpoints come from the environment. Adding a fourth vendor is a new class plus a registry entry — no change to the dispatcher.
  • Handle deferred acknowledgement honestly. A poll-based vendor cannot confirm delivery synchronously. Return deferred=True and reconcile on the next poll rather than reporting a success the vendor has not confirmed.
  • Isolate rate limits per adapter. Each vendor’s budget is independent, so one platform’s throttling must not stall delivery to the others. Give each adapter its own client and limiter.
  • Survive partial outages. When one vendor’s API is down, the registry keeps delivering to the others; the failing adapter’s events go to a dead-letter queue for controlled replay once it recovers.
  • Guard id-mapping drift. A vendor can re-key a vehicle when an asset is re-provisioned, breaking a cached VIN-to-native-id mapping. Re-resolve on a delivery failure that looks like an unknown-vehicle error rather than trusting a stale cache.
  • One protocol, many concretes — every adapter satisfies TelematicsAdapter; the dispatcher never branches on vendor name.
  • Registry by enum — vendors resolve through a registry keyed on the Vendor enum; an unknown vendor raises, never silently drops.
  • Config injected — adapters are built from AdapterConfig; no hard-coded credentials or endpoints.
  • Compliance fields preservedseverity_score, compliance_action, and audit_hash survive translation unchanged, verified against the hash.
  • Rate limits isolated — each adapter owns its client and limiter so one vendor’s throttle never stalls another.
  • Deferred acks are honest — poll-based vendors return deferred=True and reconcile later rather than faking synchronous success.
  • Outages contained — a failing vendor dead-letters its events; the others keep delivering.
Why put an adapter layer in front of three telematics APIs at all?

Because the three vendors differ on auth, id scheme, rate limits, and payload shape, and hard-coding any one of them couples your entire delivery path to that vendor. An adapter layer hides those differences behind one internal protocol, so the code that emits a DvirComplianceEvent never changes when you add a customer on a different platform or when a vendor alters its API.

How do you add a fourth vendor without touching the core?

Write one new concrete class that satisfies the TelematicsAdapter protocol and register it under a new Vendor enum value. The dispatcher resolves adapters through the registry, so it picks up the new vendor without modification. Nothing above the adapter references a vendor by name, which is what keeps the addition local to one class and one registry entry.

What happens when one vendor’s API goes down?

The registry keeps delivering to the healthy vendors while the failing adapter’s events route to a dead-letter queue for controlled replay once the vendor recovers. Because each adapter has its own client and rate limiter, one platform’s outage or throttling does not stall delivery to the others, and no out-of-service event is dropped in the process.

Does the adapter change the compliance meaning of an event?

No. An adapter translates the delivery envelope — auth, id mapping, payload shape — but the severity_score, compliance_action, and audit_hash pass through unchanged and are verified against the hash. A translation that would alter a compliance-bearing field is rejected, so a 70-and-above out-of-service event reads as out-of-service on every platform.

Back to Telematics, ELD & Maintenance Platform Integration.