Developer documentation

Steady North Decision API

Ask before you act. Send a subject and what you know about it; get back a decision, the exact rules and versions that produced it, what evidence was missing, and a receipt you can verify later. The engine never answers “compliant” — it answers with requirements you can act on.

Base URL

https://www.steadynorthapp.com/api/v1

One origin for both environments. A key issued as sn_sbx_ evaluates against the same engine and rule library but is marked sandbox on every decision and receipt it produces; sn_live_ keys are production. Nothing about the request changes — the key decides.

Quickstart

Four steps, a few minutes, no SDK.

  1. 1. Get a key. Dashboard → Settings → API keys. Scope it to what you need; it is shown once and stored as a hash.
  2. 2. Make a request. Either /evaluate (products) or /age/evaluate (app features).
  3. 3. Read the decision. Act on requirements and missingEvidence, not on a score.
  4. 4. Keep the receipt id. It proves later what rules and evidence produced that answer.

Authentication

Authorization: Bearer sn_sbx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

That value is a shape, not a working key — we do not publish an executable one. Keys are organization-scoped: a key can only ever see its own organization’s products, apps and receipts.

CertBridge — product readiness

POST/api/v1/evaluatePRODUCTION

scope: certbridge:write

Send what you know about a product. Get every applicable requirement, which ones are unsatisfied, and the specific evidence that would satisfy them. Requirements carry the rule id, the exact version, and a link to the authority.

curl -s https://www.steadynorthapp.com/api/v1/evaluate \
  -H "Authorization: Bearer sn_sbx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "content-type: application/json" \
  -d '{
    "module": "CERTBRIDGE",
    "subjectLabel": "Wooden Rattle",
    "jurisdictions": ["US"],
    "context": {
      "isChildrensProduct": true,
      "category": "TOYS",
      "manufactureLocation": "CN",
      "importedIntoUS": true,
      "subjectToCpscRule": "YES"
    },
    "evidence": [
      { "key": "field:manufacturer_name", "value": "Acme Toys" }
    ]
  }'

Response

{
  "decision": "ACTION_REQUIRED",
  "reason": "8 of 8 applicable requirements unsatisfied. Driving rule:
             SN-CPSC-005 v3 — Determine the applicable rules, then list
             every one of them separately on the certificate.",
  "requirements": [
    {
      "ruleId": "SN-CPSC-006",
      "version": 2,
      "title": "Manufacturer identification at place of manufacture",
      "outcome": "UNSATISFIED",
      "requiredAction": "Record the manufacturer's name plus full address,
                         email, and phone (§1110.11(a)(5) detail).",
      "severity": "HIGH",
      "sourceUrl": "https://www.federalregister.gov/documents/2025/01/08/..."
    }
  ],
  "missingEvidence": [
    { "key": "field:manufacturer_address",
      "label": "Manufacturer full address and contact",
      "ruleId": "SN-CPSC-006" }
  ],
  "coverageGaps": [],
  "knowledge": {
    "trustedCount": 7,
    "withheldCount": 1,
    "withheld": [
      { "ruleId": "SN-CPSC-030", "version": 1, "code": "QUARANTINED",
        "reason": "Rule SN-CPSC-030 is held at EMERGENCY_REVIEW pending
                   human review, so it cannot support a clean decision." }
    ],
    "degraded": false
  },
  "engineVersion": "1.0.0",
  "ruleSetVersion": "...",
  "evaluatedAt": "2026-08-27T02:47:48.352Z",
  "decisionId": "dec_...",
  "receiptId": "SN-RCP-3276AD4333F4"
}

Any missing legal coverage returns UNKNOWN with each gap listed in coverageGaps. A PLATFORM:* or GLOBAL selector can choose rules, but it cannot substitute for a legal jurisdiction.

A rule with outcome WITHHELD applied to your subject, but we do not yet stand behind it enough to let it clear you — it is under human review, or its source is stale, expired, legally inactive, or its fidelity has not been assessed. Withholding is one-directional: it can never produce a clean answer, and anything adverse the rule found still counts. Do not surface a withheld rule's requiredAction to your users as a task; it is a statement about our confidence, not about their record.

AgeBridge — feature decisions

POST/api/v1/age/evaluatePRODUCTION

scope: agebridge:evaluate

Speak in app terms: platform, feature, age signal, jurisdiction. Age signals are ranges, never birth dates — do not send one. The example registers the app inline; apps saved in the dashboard pass appId instead.

curl -s https://www.steadynorthapp.com/api/v1/age/evaluate \
  -H "Authorization: Bearer sn_sbx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "content-type: application/json" \
  -d '{
    "app": { "name": "MyApp", "category": "SOCIAL", "minorsAllowed": true },
    "platform": "ANDROID",
    "jurisdictions": ["US", "US-TX"],
    "feature": {
      "key": "direct_messaging",
      "capabilities": ["MESSAGING", "SOCIAL"]
    },
    "ageSignal": "13_15",
    "parentApprovalState": "NOT_REQUESTED"
  }'

Decisions are the enum, not booleans: ALLOW, REQUIRE_PARENT_APPROVAL, BLOCK, UNKNOWN. Treat anything you do not recognise as a block.

Receipts

POST/api/v1/receipts/verifyPRODUCTION

Answers one question: has this record changed since it was issued? Deliberately keyless — the receipt id is itself the high-entropy reference — and it returns only validity, never the underlying evidence.

curl -s https://www.steadynorthapp.com/api/v1/receipts/verify \
  -H "content-type: application/json" \
  -d '{ "receiptId": "SN-RCP-3276AD4333F4" }'

# → {
#     "receiptId": "SN-RCP-3276AD4333F4",
#     "valid": true,
#     "checks": [
#       { "name": "Payload hash", "ok": true },
#       { "name": "Signature", "ok": true },
#       { "name": "Receipt id embedded in payload", "ok": true }
#     ],
#     "issuedAt": "2026-08-27T02:47:48.352Z"
#   }
GET/api/v1/receipts/{id}PRODUCTION

scope: receipts:read

The full canonical payload — decision, subject, jurisdictions, every rule and exact version applied, evidence considered, evidence missing, engine version, chain index — plus a fresh verification. Organization-scoped: your key sees only your receipts. Receipts issued by the free public checkers carry no organization and no personal data, and are readable without a key by design.

Webhooks

POSTyour endpointPRODUCTION

Register endpoints in Settings → Webhooks. Every delivery carries x-sn-event, x-sn-delivery and x-sn-signature. Failed deliveries retry with exponential backoff (1m → 64m, five attempts).

Verify the signature before trusting a delivery. The header is t=<unix seconds>,v1=<hex>, where the hex is HMAC-SHA256(secret, "<t>.<raw body>"). Sign the raw body — re-serializing the parsed JSON changes the bytes and the signature will not match.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyWebhook(rawBody: string, header: string, secret: string) {
  const parts = Object.fromEntries(
    header.split(",").map((kv) => kv.split("=") as [string, string]),
  );
  const t = parts.t;
  const given = parts.v1;
  if (!t || !given) return false;

  // Reject stale deliveries so a captured request cannot be replayed later.
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;

  const expected = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(given, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}

Events

decision.created · decision.changed · filing.status_changed · recall.detected · parent_approval_required · product.action_required · rule.changed

Errors

StatusMeansDo
400Input failed validationRead detail.fieldErrors — it names each field
401Missing or unrecognised keyCheck the Authorization header
403Key lacks the required scopeThe message names the scope; re-issue the key with it
402Plan limit reachedBody carries used, limit and upgradeTo
500Our faultQuote x-request-id from the response headers

Filing SANDBOX

Filing endpoints build and validate a normalized certificate filing against CPSC’s required fields, then submit it to CPSC’s sandbox. Every filing record and response is labeled SANDBOX. Production Product Registry access is user-delegated — it uses your own Registry credentials, not ours — and is not connected yet. Validation results are real regardless of environment.

SDKs PLANNED

There are no downloadable SDKs today. The planned AgeBridge Swift and Kotlin adapters would normalize native platform signals (Apple’s declared age range, Google Play age signals) into the ageSignal vocabulary on-device, then call /age/evaluate. Native signals are never fabricated server-side. Until those exist, call the HTTP API directly — it is the same surface the SDKs would wrap.

Steady North provides compliance workflow software and decision support. It is not a law firm and does not provide legal advice. Every rule links to the authority it was drawn from — read the source.