Hazmat Customer API Documentation

Machine-to-machine REST API for programmatic access to the PHMSA hazardous materials database and placard advisory engine. All endpoints require a valid OAuth 2.0 Bearer token issued by Auth0.

Base URL https://api.hazmat-tech.com v1
🔐 Authentication — OAuth 2.0 Client Credentials (M2M)

Every API endpoint is protected with OAuth 2.0 Bearer tokens issued by Auth0 using the Client Credentials (machine-to-machine) grant type. Your backend application exchanges a Client ID and Client Secret for a short-lived signed JWT, then sends that token in every API request as a standard HTTP Authorization header.

Step 1 — Request an Access Token

POST to the Auth0 token endpoint shown below. Replace the placeholder values with the credentials shown in your Account page. The Auth0 Domain and Audience values are provided alongside your credentials on the Account page.

Token Request — HTTP
POST https://{YOUR_AUTH0_DOMAIN}/oauth/token
Content-Type: application/json

{
  "client_id":     "{YOUR_CLIENT_ID}",
  "client_secret": "{YOUR_CLIENT_SECRET}",
  "audience":      "{YOUR_API_AUDIENCE}",
  "grant_type":    "client_credentials"
}

A successful 200 OK response returns:

Token Response — JSON
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type":   "Bearer",
  "expires_in":   86400
}
Step 2 — Use the Token in API Calls

Add the access_token value as a Bearer token in the Authorization header of every API request:

Request Header
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Token Lifetime: Tokens are typically valid for 86,400 seconds (24 hours). Cache and reuse a token until it is close to expiry rather than requesting a new one for every API call. Implement proactive renewal when the remaining lifetime falls below 5 minutes to avoid 401 errors mid-workflow.
🗝 API Credentials — Where to Find & Renew Them

API credentials are available exclusively to subscribers on an API + Interactive plan. To locate your Client ID, Client Secret, Auth0 Domain, and Audience:

  1. Sign in to the Hazmat Tech portal and open Account Information from the top navigation menu (your name → Account Info).
  2. Scroll to the API Credentials card. This card is visible only when you have an active API + Interactive subscription.
  3. If credentials have not yet been provisioned, click Provision API Credentials. Your Client ID will be displayed immediately. The Client Secret is shown only once — copy and store it securely (password manager, Azure Key Vault, AWS Secrets Manager, HashiCorp Vault, etc.) before leaving the page.
  4. The Auth0 Domain and Audience values are shown below your credentials. Use them verbatim in the token request.
Rotating the Client Secret

Click Rotate Secret on the Account page at any time to invalidate the current secret and generate a replacement. The previous secret is immediately revoked — update all deployed applications before rotating to avoid service interruption.

Security: Never embed credentials in client-side code, source control, build logs, or hard-coded configuration files. Store them exclusively in environment variables or a dedicated secrets management service.
🛡 API Scopes

Auth0 automatically includes all scopes granted to your M2M application in every token. The two scopes recognised by the Hazmat Customer API are:

Scope Purpose Grants access to
read:Information Read hazmat item data GET /api/v1/HazmatItem/{id}
GET /api/v1/HazmatItem/search/{term}
GET /api/v1/HazmatItem/{unId}/packagegroup/{group?}
read:Placards Placard advisory calculations POST /api/v1/PlacardAdvisor/calculate
GET /api/v1/HazmatItem/{id} read:Information

Retrieves the complete hazmat item record for a specific internal identifier.

Use the id returned by the Search or Get by UN ID endpoints to fetch the full detail record, including label codes, special provisions, packaging references, and vessel stowage information.

Path Parameters
NameTypeDescription
id uuid required Unique identifier of the hazmat item.
Responses
StatusMeaning
200 OKReturns a HazmatItemDto object.
404 Not FoundNo item exists for the supplied id.
401 UnauthorizedBearer token is missing or invalid.
403 ForbiddenToken is valid but lacks the read:Information scope.
Code Samples
# 1. Get a token
TOKEN=$(curl -s -X POST https://{YOUR_AUTH0_DOMAIN}/oauth/token \
  -H 'Content-Type: application/json' \
  -d '{"client_id":"{YOUR_CLIENT_ID}","client_secret":"{YOUR_CLIENT_SECRET}",
       "audience":"{YOUR_API_AUDIENCE}","grant_type":"client_credentials"}' \
  | jq -r '.access_token')

# 2. Fetch a hazmat item by ID
curl -X GET \
  'https://hzmcustomerapi-auh9h4afhddkgme4.centralus-01.azurewebsites.net/api/v1/HazmatItem/{ITEM_ID}' \
  -H "Authorization: Bearer $TOKEN"
// Get Hazmat Item by ID — .NET 10 console app
// Create: dotnet new console -o GetById && cd GetById
// Run:    dotnet run

using System.Net.Http.Headers;
using System.Text.Json;

const string Auth0Domain  = "{YOUR_AUTH0_DOMAIN}";  // e.g. dev-abc123.us.auth0.com
const string ClientId     = "{YOUR_CLIENT_ID}";
const string ClientSecret = "{YOUR_CLIENT_SECRET}";
const string Audience     = "{YOUR_API_AUDIENCE}";
const string ApiBase      = "https://hzmcustomerapi-auh9h4afhddkgme4.centralus-01.azurewebsites.net";

// ── Step 1: Obtain an M2M access token ──────────────────────────────
using var http = new HttpClient();

var tokenResp = await http.PostAsync(
    $"https://{Auth0Domain}/oauth/token",
    new FormUrlEncodedContent(new Dictionary<string, string>
    {
        ["client_id"]     = ClientId,
        ["client_secret"] = ClientSecret,
        ["audience"]      = Audience,
        ["grant_type"]    = "client_credentials"
    }));

tokenResp.EnsureSuccessStatusCode();
var accessToken = JsonDocument.Parse(await tokenResp.Content.ReadAsStringAsync())
    .RootElement.GetProperty("access_token").GetString()!;

// ── Step 2: Call GET /api/v1/HazmatItem/{id} ────────────────────────
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", accessToken);

const string ItemId = "{PASTE_A_VALID_ITEM_GUID_HERE}";
var resp = await http.GetAsync($"{ApiBase}/api/v1/HazmatItem/{ItemId}");

Console.WriteLine($"Status: {(int)resp.StatusCode} {resp.StatusCode}");
var body = await resp.Content.ReadAsStringAsync();
Console.WriteLine(JsonSerializer.Serialize(
    JsonDocument.Parse(body).RootElement,
    new JsonSerializerOptions { WriteIndented = true }));
// get-by-id.mjs — requires Node.js 18+ (built-in fetch)
// Run: node get-by-id.mjs

const AUTH0_DOMAIN    = '{YOUR_AUTH0_DOMAIN}';
const CLIENT_ID       = '{YOUR_CLIENT_ID}';
const CLIENT_SECRET   = '{YOUR_CLIENT_SECRET}';
const AUDIENCE        = '{YOUR_API_AUDIENCE}';
const API_BASE        = 'https://hzmcustomerapi-auh9h4afhddkgme4.centralus-01.azurewebsites.net';

// Step 1: Obtain M2M access token
async function getToken() {
  const res = await fetch(`https://${AUTH0_DOMAIN}/oauth/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      client_id: CLIENT_ID, client_secret: CLIENT_SECRET,
      audience:  AUDIENCE,  grant_type:    'client_credentials'
    })
  });
  if (!res.ok) throw new Error(`Token request failed: ${res.status}`);
  const data = await res.json();
  return data.access_token;
}

// Step 2: Fetch hazmat item by ID
const token  = await getToken();
const itemId = '{PASTE_A_VALID_ITEM_GUID_HERE}';

const resp = await fetch(`${API_BASE}/api/v1/HazmatItem/${itemId}`, {
  headers: { Authorization: `Bearer ${token}` }
});

console.log(`Status: ${resp.status}`);
const data = await resp.json();
console.log(JSON.stringify(data, null, 2));
GET /api/v1/HazmatItem/{unId}/packagegroup/{packageGroup?} read:Information

Returns all full-detail hazmat records matching a UN/NA identification number, optionally filtered by packing group.

A single UN number can map to multiple rows in the Hazardous Materials Table (one per packing group or transport category). Omit packageGroup to retrieve all rows for the number.

Path Parameters
NameTypeDescription
unId string required UN or NA identification number including prefix, e.g. UN1203, NA1993.
packageGroup string optional Packing group filter. Accepted values: I, II, III. Omit to return all packing groups.
Responses
StatusMeaning
200 OKReturns an array of HazmatItemDto. Empty array when no matches found.
400 Bad RequestThe unId parameter failed validation.
401 UnauthorizedBearer token is missing or invalid.
403 ForbiddenToken lacks the read:Information scope.
Code Samples
# 1. Get a token
TOKEN=$(curl -s -X POST https://{YOUR_AUTH0_DOMAIN}/oauth/token \
  -H 'Content-Type: application/json' \
  -d '{"client_id":"{YOUR_CLIENT_ID}","client_secret":"{YOUR_CLIENT_SECRET}",
       "audience":"{YOUR_API_AUDIENCE}","grant_type":"client_credentials"}' \
  | jq -r '.access_token')

# 2. Look up UN1203, packing group III
curl -X GET \
  'https://hzmcustomerapi-auh9h4afhddkgme4.centralus-01.azurewebsites.net/api/v1/HazmatItem/UN1203/packagegroup/III' \
  -H "Authorization: Bearer $TOKEN"

# Omit the package group segment to return all packing groups:
# .../HazmatItem/UN1203/packagegroup
// Get Hazmat Items by UN ID and Package Group — .NET 10 console app
// Create: dotnet new console -o GetByUnId && cd GetByUnId
// Run:    dotnet run

using System.Net.Http.Headers;
using System.Text.Json;

const string Auth0Domain  = "{YOUR_AUTH0_DOMAIN}";
const string ClientId     = "{YOUR_CLIENT_ID}";
const string ClientSecret = "{YOUR_CLIENT_SECRET}";
const string Audience     = "{YOUR_API_AUDIENCE}";
const string ApiBase      = "https://hzmcustomerapi-auh9h4afhddkgme4.centralus-01.azurewebsites.net";

using var http = new HttpClient();

// Step 1: Get token
var tokenResp = await http.PostAsync(
    $"https://{Auth0Domain}/oauth/token",
    new FormUrlEncodedContent(new Dictionary<string, string>
    {
        ["client_id"] = ClientId, ["client_secret"] = ClientSecret,
        ["audience"]  = Audience, ["grant_type"]    = "client_credentials"
    }));
tokenResp.EnsureSuccessStatusCode();
var token = JsonDocument.Parse(await tokenResp.Content.ReadAsStringAsync())
    .RootElement.GetProperty("access_token").GetString()!;

// Step 2: Fetch UN1203, packing group III
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", token);

const string UnId  = "UN1203";
const string PG    = "III";    // omit to return all packing groups

var resp = await http.GetAsync($"{ApiBase}/api/v1/HazmatItem/{UnId}/packagegroup/{PG}");

Console.WriteLine($"Status: {(int)resp.StatusCode}");
Console.WriteLine(JsonSerializer.Serialize(
    JsonDocument.Parse(await resp.Content.ReadAsStringAsync()).RootElement,
    new JsonSerializerOptions { WriteIndented = true }));
// get-by-unid.mjs — requires Node.js 18+
// Run: node get-by-unid.mjs

const AUTH0_DOMAIN    = '{YOUR_AUTH0_DOMAIN}';
const CLIENT_ID       = '{YOUR_CLIENT_ID}';
const CLIENT_SECRET   = '{YOUR_CLIENT_SECRET}';
const AUDIENCE        = '{YOUR_API_AUDIENCE}';
const API_BASE        = 'https://hzmcustomerapi-auh9h4afhddkgme4.centralus-01.azurewebsites.net';

async function getToken() {
  const res = await fetch(`https://${AUTH0_DOMAIN}/oauth/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      client_id: CLIENT_ID, client_secret: CLIENT_SECRET,
      audience:  AUDIENCE,  grant_type:    'client_credentials'
    })
  });
  if (!res.ok) throw new Error(`Token request failed: ${res.status}`);
  return (await res.json()).access_token;
}

const token = await getToken();
const unId  = 'UN1203';
const pg    = 'III';   // omit from URL path to get all packing groups

const resp = await fetch(`${API_BASE}/api/v1/HazmatItem/${unId}/packagegroup/${pg}`, {
  headers: { Authorization: `Bearer ${token}` }
});

console.log(`Status: ${resp.status}`);
console.log(JSON.stringify(await resp.json(), null, 2));
POST /api/v1/PlacardAdvisor/calculate read:Placards

Calculates the DOT/AAR placard requirements for one or more hazmat shipment items.

Implements placarding logic derived from the AAR 091 / CalcPlcs rule set. Supply each hazmat item on the vehicle or trailer with its UN/NA number, packing group, gross weight in pounds, and whether it is a bulk package. The engine returns the exact set of placards required, any separation or co-load warnings, and fatal compatibility errors that prevent transport.

Request Headers
HeaderValue
Content-Typeapplication/json
AuthorizationBearer {access_token}
Request Body — PlacardAdvisoryRequest
FieldTypeDescription
items HazmatShipmentItem[] required One or more hazmat items to evaluate. At least one item must be present.
hasFoodItems boolean optional Set to true when food items are co-loaded on the same vehicle. Defaults to false.
HazmatShipmentItem fields
FieldTypeDescription
uNIdstringrequiredUN or NA number including prefix, e.g. UN1203.
packageGroupstringrequiredPacking group: I, II, or III.
weightnumberrequiredGross weight in pounds.
isBulkbooleanrequiredtrue for bulk packaging (tank car, cargo tank, portable tank, or bulk bag).
Example Request Body
JSON
{
  "items": [
    {
      "uNId":         "UN1203",
      "packageGroup": "III",
      "weight":       450.0,
      "isBulk":       false
    },
    {
      "uNId":         "UN1950",
      "packageGroup": "II",
      "weight":       1200.0,
      "isBulk":       true
    }
  ],
  "hasFoodItems": false
}
Responses
StatusMeaning
200 OKReturns a PlacardAdvisorResult object.
400 Bad RequestRequest body is null, items is empty, or a field failed validation.
401 UnauthorizedBearer token is missing or invalid.
403 ForbiddenToken lacks the read:Placards scope.
Interpreting the Result
  • Normal result: hasCompatibilityError is false, placards lists every required placard, and warnings lists any separation or advisory notices.
  • Fatal compatibility error: hasCompatibilityError is true and placards is empty. The errors array describes the incompatibility (e.g. explosive class conflict). The shipment cannot be transported as configured.
  • Placard images: Each PlacardInfo object may include a imageBase64 field containing a Base64-encoded PNG of the official placard graphic. Decode and render it directly in your application.
Code Samples
# 1. Get a token
TOKEN=$(curl -s -X POST https://{YOUR_AUTH0_DOMAIN}/oauth/token \
  -H 'Content-Type: application/json' \
  -d '{"client_id":"{YOUR_CLIENT_ID}","client_secret":"{YOUR_CLIENT_SECRET}",
       "audience":"{YOUR_API_AUDIENCE}","grant_type":"client_credentials"}' \
  | jq -r '.access_token')

# 2. Calculate placards for a mixed shipment
curl -X POST \
  'https://hzmcustomerapi-auh9h4afhddkgme4.centralus-01.azurewebsites.net/api/v1/PlacardAdvisor/calculate' \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "items": [
      {"uNId":"UN1203","packageGroup":"III","weight":450.0,"isBulk":false},
      {"uNId":"UN1950","packageGroup":"II","weight":1200.0,"isBulk":true}
    ],
    "hasFoodItems": false
  }'
// Calculate Placards — .NET 10 console app
// Create: dotnet new console -o PlacardAdvisor && cd PlacardAdvisor
// Run:    dotnet run

using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

const string Auth0Domain  = "{YOUR_AUTH0_DOMAIN}";
const string ClientId     = "{YOUR_CLIENT_ID}";
const string ClientSecret = "{YOUR_CLIENT_SECRET}";
const string Audience     = "{YOUR_API_AUDIENCE}";
const string ApiBase      = "https://hzmcustomerapi-auh9h4afhddkgme4.centralus-01.azurewebsites.net";

using var http = new HttpClient();

// Step 1: Get token
var tokenResp = await http.PostAsync(
    $"https://{Auth0Domain}/oauth/token",
    new FormUrlEncodedContent(new Dictionary<string, string>
    {
        ["client_id"] = ClientId, ["client_secret"] = ClientSecret,
        ["audience"]  = Audience, ["grant_type"]    = "client_credentials"
    }));
tokenResp.EnsureSuccessStatusCode();
var token = JsonDocument.Parse(await tokenResp.Content.ReadAsStringAsync())
    .RootElement.GetProperty("access_token").GetString()!;

// Step 2: POST to PlacardAdvisor/calculate
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", token);

var requestBody = new
{
    items = new[]
    {
        new { uNId = "UN1203", packageGroup = "III", weight = 450.0,  isBulk = false },
        new { uNId = "UN1950", packageGroup = "II",  weight = 1200.0, isBulk = true  }
    },
    hasFoodItems = false
};

var json = JsonSerializer.Serialize(requestBody);
var resp = await http.PostAsync(
    $"{ApiBase}/api/v1/PlacardAdvisor/calculate",
    new StringContent(json, Encoding.UTF8, "application/json"));

Console.WriteLine($"Status: {(int)resp.StatusCode}");
Console.WriteLine(JsonSerializer.Serialize(
    JsonDocument.Parse(await resp.Content.ReadAsStringAsync()).RootElement,
    new JsonSerializerOptions { WriteIndented = true }));
// placard-advisor.mjs — requires Node.js 18+
// Run: node placard-advisor.mjs

const AUTH0_DOMAIN    = '{YOUR_AUTH0_DOMAIN}';
const CLIENT_ID       = '{YOUR_CLIENT_ID}';
const CLIENT_SECRET   = '{YOUR_CLIENT_SECRET}';
const AUDIENCE        = '{YOUR_API_AUDIENCE}';
const API_BASE        = 'https://hzmcustomerapi-auh9h4afhddkgme4.centralus-01.azurewebsites.net';

async function getToken() {
  const res = await fetch(`https://${AUTH0_DOMAIN}/oauth/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      client_id: CLIENT_ID, client_secret: CLIENT_SECRET,
      audience:  AUDIENCE,  grant_type:    'client_credentials'
    })
  });
  if (!res.ok) throw new Error(`Token request failed: ${res.status}`);
  return (await res.json()).access_token;
}

const token = await getToken();

const requestBody = {
  items: [
    { uNId: 'UN1203', packageGroup: 'III', weight: 450.0,  isBulk: false },
    { uNId: 'UN1950', packageGroup: 'II',  weight: 1200.0, isBulk: true  }
  ],
  hasFoodItems: false
};

const resp = await fetch(`${API_BASE}/api/v1/PlacardAdvisor/calculate`, {
  method:  'POST',
  headers: {
    Authorization:  `Bearer ${token}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(requestBody)
});

console.log(`Status: ${resp.status}`);
const result = await resp.json();
console.log('Placards:',  result.placards?.map(p => p.description));
console.log('Warnings:',  result.warnings);
console.log('Errors:',    result.errors);
console.log('Fatal:',     result.hasCompatibilityError);
📐 Data Models
HazmatItemDto

Returned by Get by ID and Get by UN ID.

FieldTypeDescription
iduuidUnique item identifier.
uNIdstringUN or NA identification number, e.g. UN1203.
descriptionstringOfficial proper shipping name from the Hazardous Materials Table.
classstringHazard class or division, e.g. 3, 2.1, 6.1.
packageGroupstringPacking group: I, II, III, or empty.
properNamesstring[]Alternate proper shipping names associated with this entry.
labelCodesobjectKey/value pairs of label code to label description.
symbolsProvisionDetailDto[]Table symbols with code, description, and optional regulatory URL.
specialProvisionsProvisionDetailDto[]Special provisions applicable to this material.
packagingExceptionsobjectPackaging exception references (key → URL).
packagingNonBulkobjectNon-bulk packaging authorization references (key → URL).
packagingBulkobjectBulk packaging authorization references (key → URL).
quantityLimitsPassengernumberPassenger aircraft/rail quantity limit.
quantityLimitsCargonumberCargo aircraft only quantity limit.
unitOfMeasurestringUnit for quantity limits (e.g. kg, L).
vesselStowageLocationProvisionDetailDto[]Authorized vessel stowage location codes.
vesselStowageOtherProvisionDetailDto[]Additional vessel stowage handling codes.
HazmatItemSearchDto

Returned by the Search endpoint.

FieldTypeDescription
iduuidUnique identifier — use to fetch full detail via Get by ID.
uNIdstringUN or NA identification number.
descriptionstringProper shipping name.
classstringHazard class or division.
packageGroupstringPacking group.
properNamesstring[]Alternate proper shipping names.
ProvisionDetailDto

Used within HazmatItemDto for symbols, special provisions, and vessel stowage codes.

FieldTypeDescription
codestringShort code identifier, e.g. A, T1, W12.
descriptionstringFull human-readable description of the provision or symbol.
urlstring | nullLink to the regulatory reference (eCFR), when available.
PlacardAdvisoryRequest

Request body for POST /PlacardAdvisor/calculate.

FieldTypeDescription
itemsHazmatShipmentItem[]One or more hazmat items loaded on the vehicle. At least one required.
hasFoodItemsbooleanWhether food items are co-loaded on the same vehicle.
HazmatShipmentItem
FieldTypeDescription
uNIdstringUN or NA number including prefix.
packageGroupstringPacking group: I, II, or III.
weightnumberGross weight in pounds.
isBulkbooleantrue for bulk packaging (tank cars, cargo tanks, portable tanks, or bulk bags).
PlacardAdvisorResult

Response body from POST /PlacardAdvisor/calculate.

FieldTypeDescription
placardsPlacardInfo[]Required placards. Empty when hasCompatibilityError is true.
warningsstring[]Non-fatal notices (separation required, unrecognised UN numbers, etc.).
errorsstring[]Fatal compatibility errors preventing transport as configured.
hasCompatibilityErrorbooleantrue when a COMRULE 'X' conflict was detected.
PlacardInfo
FieldTypeDescription
descriptionstringPlacard display name, e.g. FLAMMABLE LIQUID.
imageBase64string | nullBase64-encoded PNG of the placard graphic, or null if no image is stored.
API versioning is communicated via the URL path segment (v1). Optionally, include an X-Api-Version: 1.0 header or ?api-version=1.0 query string parameter. All timestamps are in UTC. All weights are in pounds.
An unhandled error has occurred. Reload 🗙