Using the REST API
Prerequisites
Section titled “Prerequisites”- Before you can use the TMM REST API, you must first Request an Application ID for your application.
- Your Application ID uniquely identifies your application.
- Your Application ID comes int the form of a UUID.
- Do not share your Application ID with anyone outside of your development team.
Access Codes
Section titled “Access Codes”TMM API access codes are used to authorize applications to access the TMM REST API. The client application generates an access code and includes it in the HTTP Authorization header of the request. TMM will validate the access code against all registered client applications. If the code checks out, then TMM will process the request. If the code does not check out, then TMM will respond with 401 Unauthorized.
New integrators should use Access Code V2. We will continue to support Access Code V1 for existing integrations.
The following access codes are available:
Access Code V2
- Recommended for all new integrations.
- A long lived token, valid for 60 minutes
- For secure connections, can be used multiple times until it expires.
- For insecure connections, can be used only once.
- Automatically registers the client application with TMM REST API server when the access code is first used.
- Can be used with native applications and web applications.
Access Code V1
- Continues to be supported for existing integrations.
- A short lived token, valid for 1 second
- Requires a manual client application registration request to be accepted.
- Can be used with native applications only.
- Requires client application registration before use.
- iOS: tmmRegister URL scheme
- Android: REGISTER Intent
- Windows: tmmRegister request
Access Code V1
Section titled “Access Code V1”- Access Code V1 is generated by formatting your Application ID with the current UTC time, hashing the result with SHA256, and encoding the hash in Base64.
- TMM will check the access code against all registered client applications. If the code checks out, then TMM will process the request. If the code does not check out, then TMM will respond with 401 Unauthorized.
- The access code is valid for 1 second, so it must be generated at the time of the request.
- When debugging, avoid setting a breakpoint between generating the access code and sending the request, as this will cause the access code to expire before it is sent.
Access Code V1 Generation
Section titled “Access Code V1 Generation”public static string GenerateAccessCode(string appID, DateTime utcTime){ string lowercaseID = appID.ToLowerInvariant(); // Format utcTime as an ISO8601 compliant string, like this: // 2024-03-15T18:42:31Z string iso8601Time = utcTime.ToString("yyyy-MM-dd'T'HH:mm:ssK", CultureInfo.InvariantCulture); string plaintextAccessCode = lowercaseID + iso8601Time; byte[] utf8Bytes = Encoding.UTF8.GetBytes(plaintextAccessCode); byte[] hashedBytes = SHA256.HashData(utf8Bytes); string base64String = Convert.ToBase64String(hashedBytes); return base64String;}struct AccessCodeGenerator { static func generateAccessCode(appID: String, utcTime: Date) -> String? { let lowercaseID = appID.lowercased()
// Format utcTime as an ISO8601 compliant string let iso8601TimeFormatter = ISO8601DateFormatter() iso8601TimeFormatter.timeZone = TimeZone(secondsFromGMT: 0) let iso8601Time = iso8601TimeFormatter.string(from: utcTime)
let plaintextAccessCode = lowercaseID + iso8601Time guard let utf8Data = plaintextAccessCode.data(using: .utf8) else { return nil }
var hashedBytes = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) utf8Data.withUnsafeBytes { _ = CC_SHA256($0.baseAddress, CC_LONG(utf8Data.count), &hashedBytes) }
let hashedData = Data(hashedBytes) let base64String = hashedData.base64EncodedString() return base64String }}fun generateAccessCode(appID: String, utcTime: Date): String { // Generates the Access Code from the app id and the current time. // Used when trying to access the receiver API or any API that requires it. // Is valid for 1 second. val lowercaseID = appID.lowercase(Locale.getDefault())
val iso8601Format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") } val iso8601Time = iso8601Format.format(utcTime)
val plaintextAccessCode = lowercaseID + iso8601Time val utf8Bytes = plaintextAccessCode.toByteArray(Charsets.UTF_8) val hashedBytes = MessageDigest.getInstance("SHA-256").digest(utf8Bytes) val base64String = Base64.getEncoder().encodeToString(hashedBytes)
return base64String}To test and verify your access code generation code, we have provided you with access-code-gold-file.json, which contains a set of random ApplicationID + UTC Time and the access code they should generate. The data looks like this:
{ "ApplicationID": "ce1e7a1e-3128-4f63-b829-223c7cb7ca1d", "UtcTime": "2074-03-16T08:46:27Z", "AccessCode": "BF4hkCZWjxyw2WN9xVPHj7gpyxrS3CRUX0BaXutzw14="}HTTP Authorization Header
Section titled “HTTP Authorization Header”When you have generated an access code, insert it into the HTTP Authorization header, using the Basic scheme. Upon receiving the request, TMM will validate the Access Code against all registered client applications. If the Code checks out, then TMM will process the request. If the code does not check out, then TMM will respond with 401 Unauthorized.
Example Request:
GET api/v1/tmmInfo HTTP/1.1Authorization: Basic BF4hkCZWjxyw2WN9xVPHj7gpyxrS3CRUX0BaXutzw14=Access Code V2
Section titled “Access Code V2”- V2 access codes use public key encryption, your Application ID, and the current UTC time.
- V2 access codes are valid for 60 minutes.
- For secure connections, V2 access codes can be used multiple times until they expire.
- For insecure connections, V2 access codes can be used only once.
Getting the Public Key
Section titled “Getting the Public Key”In order to generate an Access Code V2, you must first retrieve the public key from the TMM REST API server. The public key is used to encrypt the plain text access code, which is then sent to the REST API server for validation.
- TMM generates a public/private key pair that has the lifetime of the installed TMM application.
- Query the public key from the TMM REST API server using the api/v1/publicKey endpoint.
- The publicKey endpoint does not require an access code/Authorization header to be sent in the request, and is available to all applications.
- The public key is returned in JSON Web Key (JWK) format, which is a standard format for representing public keys.
Example Response:
{ "kty": "RSA", "n": "tjRGne4mEIqru489eq8BfxNpam8laClBzCbzZXsByWQZ09...", "e": "AQAB"}Access Code V2 Generation
Section titled “Access Code V2 Generation”using System.Globalization;using System.Security.Cryptography;using System.Text;using System.Text.Json;
namespace TestApp.AccessCode;
static public class AccessCodeV2{ private static RSA _rsa;
public static void SetPublicKey(string jwkJson) { ArgumentException.ThrowIfNullOrWhiteSpace(jwkJson);
using JsonDocument document = JsonDocument.Parse(jwkJson); JsonElement root = document.RootElement;
if (root.GetProperty("kty").GetString() != "RSA") { throw new ArgumentException("Only RSA JWK keys are supported.", nameof(jwkJson)); }
byte[] modulus = Base64UrlDecode(root.GetProperty("n").GetString()); byte[] exponent = Base64UrlDecode(root.GetProperty("e").GetString());
var rsa = RSA.Create(); rsa.ImportParameters(new RSAParameters { Modulus = modulus, Exponent = exponent, });
_rsa?.Dispose(); _rsa = rsa; }
private static byte[] Base64UrlDecode(string base64Url) { string base64 = base64Url.Replace('-', '+').Replace('_', '/'); int padding = (4 - base64.Length % 4) % 4; return Convert.FromBase64String(base64.PadRight(base64.Length + padding, '=')); }
public static string Generate(Guid appID, DateTime utcTime) { if(_rsa is null) { throw new InvalidOperationException("public key not set"); }
string lowercaseID = appID.ToString("D").ToLowerInvariant(); // Format utcTime as an ISO8601 compliant string, like this: 2024-02-22T18:00:00Z string iso8601Time = utcTime.ToString("yyyy-MM-dd'T'HH:mm:ssK", CultureInfo.InvariantCulture); string plaintextAccessCode = lowercaseID + " " + iso8601Time; byte[] utf8Bytes = Encoding.UTF8.GetBytes(plaintextAccessCode); byte[] encryptedBytes = _rsa.Encrypt(utf8Bytes, RSAEncryptionPadding.OaepSHA256); string base64String = Convert.ToBase64String(encryptedBytes); return base64String; }}enum AccessCodeV2 { private static var publicKey: SecKey?
static func setPublicKey(jwkJson: String) throws { guard !jwkJson.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { throw NSError(domain: "AccessCodeV2", code: 1, userInfo: [ NSLocalizedDescriptionKey: "JWK JSON must not be empty." ]) }
guard let data = jwkJson.data(using: .utf8), let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let kty = json["kty"] as? String, kty == "RSA", let n = json["n"] as? String, let e = json["e"] as? String else { throw NSError(domain: "AccessCodeV2", code: 2, userInfo: [ NSLocalizedDescriptionKey: "Only RSA JWK keys are supported." ]) }
let modulus = try base64UrlDecode(n) let exponent = try base64UrlDecode(e)
guard let key = createRSAPublicKey(modulus: modulus, exponent: exponent) else { throw NSError(domain: "AccessCodeV2", code: 2, userInfo: [ NSLocalizedDescriptionKey: "Only RSA JWK keys are supported." ]) }
publicKey = key }
private static func base64UrlDecode(_ base64Url: String) throws -> Data { var base64 = base64Url .replacingOccurrences(of: "-", with: "+") .replacingOccurrences(of: "_", with: "/") let padding = (4 - base64.count % 4) % 4 base64 += String(repeating: "=", count: padding) guard let data = Data(base64Encoded: base64) else { throw NSError(domain: "AccessCodeV2", code: 3, userInfo: [ NSLocalizedDescriptionKey: "Invalid base64url value." ]) } return data }
// SecKeyCreateWithData expects PKCS#1: SEQUENCE { modulus INTEGER, exponent INTEGER } private static func createRSAPublicKey(modulus: Data, exponent: Data) -> SecKey? { let keyData = derSequence([ derInteger(modulus), derInteger(exponent), ])
let attributes: [String: Any] = [ kSecAttrKeyType as String: kSecAttrKeyTypeRSA, kSecAttrKeyClass as String: kSecAttrKeyClassPublic, kSecAttrKeySizeInBits as String: modulus.count * 8, ] var error: Unmanaged<CFError>? return SecKeyCreateWithData(keyData as CFData, attributes as CFDictionary, &error) }
private static func derSequence(_ children: [Data]) -> Data { let content = children.reduce(into: Data()) { $0.append($1) } return Data([0x30]) + derLength(content.count) + content }
private static func derInteger(_ value: Data) -> Data { var bytes = Data(value) if bytes.first.map({ $0 >= 0x80 }) == true { bytes.insert(0x00, at: 0) } return Data([0x02]) + derLength(bytes.count) + bytes }
private static func derLength(_ length: Int) -> Data { if length < 128 { return Data([UInt8(length)]) } var len = length var bytes = Data() while len > 0 { bytes.insert(UInt8(len & 0xFF), at: 0) len >>= 8 } return Data([0x80 | UInt8(bytes.count)]) + bytes }
static func generate(appID: UUID, utcTime: Date) throws -> String { guard let key = publicKey else { throw NSError(domain: "AccessCodeV2", code: 4, userInfo: [ NSLocalizedDescriptionKey: "public key not set" ]) }
let lowercaseID = appID.uuidString.lowercased()
let iso8601Formatter = ISO8601DateFormatter() iso8601Formatter.timeZone = TimeZone(secondsFromGMT: 0) iso8601Formatter.formatOptions = [.withInternetDateTime] let iso8601Time = iso8601Formatter.string(from: utcTime)
let plaintextAccessCode = "\(lowercaseID) \(iso8601Time)" guard let plaintextData = plaintextAccessCode.data(using: .utf8) else { throw NSError(domain: "AccessCodeV2", code: 5, userInfo: [ NSLocalizedDescriptionKey: "Failed to encode plaintext." ]) }
var error: Unmanaged<CFError>? guard let encryptedData = SecKeyCreateEncryptedData( key, .rsaEncryptionOAEPSHA256, plaintextData as CFData, &error ) as Data? else { throw error!.takeRetainedValue() as Error }
return encryptedData.base64EncodedString() }}object AccessCodeV2 { private var publicKey: PublicKey? = null
fun setPublicKey(jwkJson: String) { require(jwkJson.isNotBlank())
val json = JSONObject(jwkJson) check(json.getString("kty") == "RSA") { "Only RSA JWK keys are supported." }
val modulus = base64UrlDecode(json.getString("n")) val exponent = base64UrlDecode(json.getString("e"))
val keySpec = RSAPublicKeySpec( BigInteger(1, modulus), BigInteger(1, exponent) ) publicKey = KeyFactory.getInstance("RSA").generatePublic(keySpec) }
private fun base64UrlDecode(base64Url: String): ByteArray { var base64 = base64Url.replace('-', '+').replace('_', '/') val padding = (4 - base64.length % 4) % 4 base64 = base64.padEnd(base64.length + padding, '=') return Base64.getDecoder().decode(base64) }
fun generate(appID: UUID, utcTime: Date): String { val key = checkNotNull(publicKey) { "public key not set" }
val lowercaseID = appID.toString().lowercase(Locale.US)
val iso8601Format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") } val iso8601Time = iso8601Format.format(utcTime)
val plaintextAccessCode = "$lowercaseID $iso8601Time" val utf8Bytes = plaintextAccessCode.toByteArray(Charsets.UTF_8)
val cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding") cipher.init(Cipher.ENCRYPT_MODE, key) val encryptedBytes = cipher.doFinal(utf8Bytes)
return Base64.getEncoder().encodeToString(encryptedBytes) }}class AccessCodeV2 {import { constants, createPublicKey, type KeyObject, publicEncrypt,} from "node:crypto";
let publicKey: KeyObject | null = null;
export interface RsaPublicJwk { kty: string; n: string; e: string; [key: string]: unknown;}
export function setPublicKey(jwk: RsaPublicJwk): void { if (jwk.kty !== "RSA") { throw new Error("Only RSA JWK keys are supported."); }
if (!jwk.n?.trim() || !jwk.e?.trim()) { throw new Error("JWK must include n and e."); }
publicKey = createPublicKey({ key: jwk, format: "jwk", });}
export function generateAccessCodeV2(appId: string, utcTime: Date): string { if (!publicKey) { throw new Error("public key not set"); }
const lowercaseId = appId.toLowerCase(); const iso8601Time = utcTime.toISOString().split('.')[0] + 'Z'; const plaintextAccessCode = `${lowercaseId} ${iso8601Time}`; const encryptedBytes = publicEncrypt( { key: publicKey, padding: constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha256", }, Buffer.from(plaintextAccessCode, "utf8"), );
return encryptedBytes.toString("base64");}
export function generateAccessCodeV2Now(appId: string): string { return generateAccessCodeV2(appId, new Date());}HTTP Authorization Header
Section titled “HTTP Authorization Header”When you have generated an access code, insert it into the HTTP Authorization header, using the AccessCodeV2 scheme. Upon receiving the request, TMM will validate the Access Code against all registered client applications. If the Code checks out, then TMM will process the request. If the code does not check out, then TMM will respond with 401 Unauthorized.
Example Request:
GET api/v1/tmmInfo HTTP/1.1Authorization: AccessCodeV2 TXJxUhsje4ZgyzAOFT30MX0ipgm7zte++8+Zk7ZIUdheTe83mt0iCDbbHDFSMkGn7gM8Wd3tgZ7abQ8OgJRkIkUmeGKZehRuf09XclXfyLKm6kNA2YZJm2xVWLBE2TekOqWSSBATmR8IyeEXGztWKA7CRliUq3iIX3XRsV2b+n4ISV83pYkHrmWHq35nHcYitWEnB3X1rv1NdVbPU5NMxeBMuGNvsEI5Sv7/4WkOX1bZmUZmpIX/GQ9mWbeOKPqF5+TBOuyvWw9bnBgrseHGXvrJgKqaG3Hbcb35jLd3EZ1HPbwHlW9uBDi4+xfOCbReT0Kc84jdhj3TujXbGDNwCA==Example: Get TMM Info
Section titled “Example: Get TMM Info”In this example, we will use a secure connection and Access Code V2 to get information about the TMM application (see GET tmmInfo).
Get the Public Key
Section titled “Get the Public Key”static async Task GetPublicKeyAsync(){ string url = "https://tmm-api-local.fieldsystems.trimble.com:9638/api/v1/publicKey";
// Instantiate the HttpClient using var client = new HttpClient(); try { Console.WriteLine("Getting public key...");
// No access code required for public key
// Make the GET request HttpResponseMessage response = await client.GetAsync(url); response.EnsureSuccessStatusCode(); // Throws if HTTP status is an error
// Read the response body as a string string jwk = await response.Content.ReadAsStringAsync();
// Set the public key for AccessCodeV2 generation AccessCodeV2.SetPublicKey(jwk);
// Parse as a generic JSON object (JsonNode/JsonObject) // This allows you to navigate the JSON dynamically without a strongly-typed class. JsonNode? jsonObject = JsonNode.Parse(jwk);
if (jsonObject != null) { // Print the formatted JSON to console Console.WriteLine(jsonObject.ToJsonString(new JsonSerializerOptions { WriteIndented = true })); } } catch (Exception e) { Console.WriteLine($"Error: {e.Message}"); }}Get TMM Info
Section titled “Get TMM Info”static async Task GetTmmInfoAsync(Guid appID){ string url = "https://tmm-api-local.fieldsystems.trimble.com:9638/api/v1/tmmInfo";
// Instantiate the HttpClient using var client = new HttpClient(); try { // Generate a V2 access code string accessCode = AccessCodeV2.Generate(appID, DateTime.UtcNow); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("AccessCodeV2", accessCode);
// Make the GET request HttpResponseMessage response = await client.GetAsync(url); response.EnsureSuccessStatusCode(); // Throws if HTTP status is an error
// Read the response body as a string string responseBody = await response.Content.ReadAsStringAsync();
// Parse as a generic JSON object (JsonNode/JsonObject) // This allows you to navigate the JSON dynamically without a strongly-typed class. JsonNode? jsonObject = JsonNode.Parse(responseBody);
if (jsonObject != null) { // Print the formatted JSON to console Console.WriteLine(jsonObject.ToJsonString(new JsonSerializerOptions { WriteIndented = true })); } } catch (Exception e) { Console.WriteLine($"Error: {e.Message}"); }}