PartnerLink Developer Documentation

Overview

PartnerLink lets your app open OilTrails directly to a partner-provided field destination. Your app or backend creates a signed Universal Link containing destination data. OilTrails verifies the payload, opens in focused PartnerLink mode, and shows the user the location, routing, weather, and well-data context available for that destination.

PartnerLink users do not need an OilTrails account or subscription to open partner-provided locations.

Canonical URL:

https://oiltrails.ca/partnerlink-open?payload=<base64url-json>&sig=<hmac>

How It Works

  1. Partner app or backend builds a JSON payload.
  2. Payload is Base64URL encoded.
  3. Encoded payload is signed with HMAC-SHA256.
  4. Partner app opens the partnerlink-open Universal Link.
  5. iOS launches OilTrails if installed.
  6. OilTrails verifies the payload with Firebase.
  7. OilTrails opens the destination in focused PartnerLink mode.

Prerequisites

Before you start coding, OilTrails will provide or confirm:

  • Partner ID: pid.
  • Shared signing secret.
  • Allowed launch modes, such as view or route.
  • Sample test locations.
  • iOS app location where the action will appear.
  • Optional partner branding details.

For production apps, prefer signing PartnerLink URLs on your backend and returning the finished URL to the iOS app. The sample Swift code signs locally for demonstration, but secrets embedded in mobile apps can be extracted.

Payload Contract

FieldRequiredTypeDescription
vYesIntegerPayload version. Must be 1.
pidYesStringOilTrails-issued partner ID.
companyNoStringPartner or customer company name.
nameYesStringHuman-readable destination name.
queryConditionalStringLSD, NTS, UWI, address, site name, or searchable location text.
latConditionalNumberLatitude. Required if lon is provided.
lonConditionalNumberLongitude. Required if lat is provided.
extidNoStringPartner-side ticket, job, work order, or record ID.
modeNoStringRequested launch mode, such as view or route. Must be enabled for the partner.
tsYesIntegerUnix timestamp in seconds.

Provide either query or both lat and lon. You may provide both query and coordinates for the best user experience.

Example Payloads

LSD-only payload:

{
  "v": 1,
  "pid": "truck_co",
  "company": "Truck Co",
  "name": "Start LSD",
  "query": "05-21-042-08W4",
  "extid": "ticket_12345",
  "mode": "route",
  "ts": 1770000000
}

Coordinate payload:

{
  "v": 1,
  "pid": "truck_co",
  "company": "Truck Co",
  "name": "Battery Site 14",
  "lat": 52.432473,
  "lon": -114.375374,
  "extid": "dispatch_8842",
  "mode": "route",
  "ts": 1770000000
}

Combined payload:

{
  "v": 1,
  "pid": "truck_co",
  "company": "Truck Co",
  "name": "Battery Site 14",
  "query": "05-21-042-08W4",
  "lat": 52.432473,
  "lon": -114.375374,
  "extid": "work_order_9911",
  "mode": "route",
  "ts": 1770000000
}

Signing Rules

The signature must be calculated as:

sig = lowercase_hex(HMAC_SHA256(payload, shared_secret))

Where:

  • payload is the Base64URL-encoded JSON string.
  • Sign the encoded payload, not the raw JSON.
  • Use HMAC-SHA256.
  • Output lowercase hex.
  • Do not modify the payload after signing.

The final URL must match this contract:

payload = base64url(json)
sig = lowercase_hex(HMAC_SHA256(payload, secret))
url = https://oiltrails.ca/partnerlink-open?payload=<payload>&sig=<sig>

Swift Implementation

This helper builds a PartnerLink URL from an encodable payload. It signs locally for demonstration only. For production integrations, use backend signing when possible.

import Foundation
import CryptoKit
import SwiftUI
import UIKit

struct PartnerLinkPayload: Codable {
    let v: Int
    let pid: String
    let company: String?
    let name: String
    let query: String?
    let lat: Double?
    let lon: Double?
    let extid: String?
    let mode: String?
    let ts: Int
}

enum PartnerLinkError: Error {
    case invalidBaseURL
    case invalidFinalURL
}

struct PartnerLinkBuilder {
    let partnerSecret: String
    let baseURL = URL(string: "https://oiltrails.ca/partnerlink-open")!

    func makeURL(payload: PartnerLinkPayload) throws -> URL {
        let encoder = JSONEncoder()
        encoder.outputFormatting = [.sortedKeys]

        let jsonData = try encoder.encode(payload)
        let encodedPayload = base64URLEncode(jsonData)
        let signature = hmacSHA256Hex(message: encodedPayload, secret: partnerSecret)

        var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)
        components?.queryItems = [
            URLQueryItem(name: "payload", value: encodedPayload),
            URLQueryItem(name: "sig", value: signature)
        ]

        guard let url = components?.url else {
            throw PartnerLinkError.invalidFinalURL
        }

        return url
    }

    private func base64URLEncode(_ data: Data) -> String {
        data.base64EncodedString()
            .replacingOccurrences(of: "+", with: "-")
            .replacingOccurrences(of: "/", with: "_")
            .replacingOccurrences(of: "=", with: "")
    }

    private func hmacSHA256Hex(message: String, secret: String) -> String {
        let key = SymmetricKey(data: Data(secret.utf8))
        let signature = HMAC<SHA256>.authenticationCode(
            for: Data(message.utf8),
            using: key
        )

        return signature.map { String(format: "%02x", $0) }.joined()
    }
}

let payload = PartnerLinkPayload(
    v: 1,
    pid: "truck_co",
    company: "Truck Co",
    name: "Battery Site 14",
    query: "05-21-042-08W4",
    lat: 52.432473,
    lon: -114.375374,
    extid: "work_order_9911",
    mode: "route",
    ts: Int(Date().timeIntervalSince1970)
)

let builder = PartnerLinkBuilder(partnerSecret: "replace_with_partner_secret")
let partnerLinkURL = try builder.makeURL(payload: payload)

SwiftUI open example:

@Environment(\.openURL) private var openURL

openURL(partnerLinkURL) { accepted in
    print("PartnerLink accepted: \(accepted)")
}

UIKit open example:

UIApplication.shared.open(partnerLinkURL)

Backend Signing Example

For production integrations, OilTrails recommends signing PartnerLink URLs on your backend. This keeps the shared PartnerLink secret out of the iOS app while still allowing the app to open the finished URL.

POST /partnerlink-url
Input: destination fields from app

Backend:
  validate user can access destination
  build payload
  set ts to now
  base64url encode payload
  sign encoded payload with partner secret
  return final URL

App:
  open returned URL

Example Node.js signing function:

import crypto from "node:crypto";

function base64url(input) {
  return Buffer.from(JSON.stringify(input))
    .toString("base64")
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/g, "");
}

function signPartnerLinkPayload(encodedPayload, secret) {
  return crypto
    .createHmac("sha256", secret)
    .update(encodedPayload)
    .digest("hex");
}

export function buildPartnerLinkURL(destination, partnerSecret) {
  const payload = {
    v: 1,
    pid: "truck_co",
    company: "Truck Co",
    name: destination.name,
    query: destination.query,
    lat: destination.lat,
    lon: destination.lon,
    extid: destination.id,
    mode: "route",
    ts: Math.floor(Date.now() / 1000),
  };

  const encodedPayload = base64url(payload);
  const sig = signPartnerLinkPayload(encodedPayload, partnerSecret);

  return `https://oiltrails.ca/partnerlink-open?payload=${encodedPayload}&sig=${sig}`;
}

Place the PartnerLink action where the user already sees the field destination:

  • Ticket row.
  • Dispatch detail.
  • Work order.
  • Map pin.
  • Site profile.
  • Route button.
  • Inline icon beside an LSD, NTS, UWI, address, or coordinate field.

The iOS app should request or build the PartnerLink URL only when the user is ready to open the destination. That keeps ts fresh and avoids stale signatures.

OilTrails opens in focused PartnerLink mode. This means users can access the partner-provided destination and relevant location context without an OilTrails account or subscription. Some full OilTrails features may be unavailable during this mode so users stay focused on the task.

This is expected behavior:

  • It keeps the user on the trusted partner destination.
  • It reduces distraction.
  • It avoids requiring an OilTrails account for partner-provided launches.
  • It avoids requiring an OilTrails subscription for partner-provided launches.
  • The complete OilTrails app remains available separately for subscribers.

Validation Checklist

Before launch, confirm:

  • Payload includes v, pid, name, and ts.
  • Payload includes query or both lat and lon.
  • Coordinates are valid ranges.
  • ts is generated immediately before signing.
  • Base64URL encoding removes = padding.
  • Signature signs the encoded payload string.
  • Signature is lowercase hex.
  • URL uses /partnerlink-open.
  • Test with OilTrails installed.
  • Test without OilTrails installed.
  • Test LSD, NTS, UWI, address, coordinate, and combined payloads.
  • Test allowed and unsupported mode values.

Error Reference

ErrorMeaningFix
missing_payloadURL has no payload.Add payload query parameter.
missing_signatureURL has no signature.Add sig query parameter.
invalid_payload_encodingPayload is not valid Base64URL.Check Base64URL conversion.
invalid_payload_jsonDecoded payload is not valid JSON.Validate payload before encoding.
unsupported_versionv is not 1.Set "v": 1.
unknown_partnerpid is not recognized.Confirm partner ID.
invalid_signatureSignature mismatch.Sign encoded payload with correct secret.
expired_requestTimestamp is stale.Generate ts immediately before signing.
disabled_partnerPartner account inactive.Contact OilTrails.
invalid_destinationNo valid query or coordinate pair.Send query or both lat and lon.
unsupported_modeMode not enabled.Use an allowed mode.

Testing Tools

Useful items to keep in your integration test notes:

  • Sample partner ID and secret for sandbox testing.
  • Test payload generator.
  • Copyable sample URLs.
  • Log format for outgoing payload JSON, encoded payload, signature, and final URL.
  • Test matrix for location formats.

Recommended test matrix:

  • LSD-only.
  • NTS-only.
  • UWI-only.
  • Address-only.
  • Coordinate-only.
  • Query plus coordinates.
  • Valid route mode.
  • Unsupported mode.
  • Expired timestamp.
  • Invalid signature.

When logging, never log the shared signing secret. It is safe to send OilTrails the payload JSON, encoded payload, signature, final URL, and error code when troubleshooting.

Legacy Compatibility

New integrations should use:

/partnerlink-open?payload=...&sig=...

Legacy /sitelink, /open-sitelink, and ?t=<token> flows may exist for compatibility but should not be used for new builds.

Support

When asking OilTrails for integration support, include:

  • Partner ID.
  • Approximate timestamp.
  • extid.
  • Payload JSON, without the secret.
  • Full error code.
  • iOS version and app version.
  • Whether OilTrails was installed.
  • Whether the failure occurred on device or simulator.
  • Whether the link was signed on backend or client.