Forge API

Connect your AI agent to any machine. Clean, normalized, governed data in, supervised action out.

Quick start

  1. Get your API key: free at /pricing (keyless Forge Connect sandbox)
  2. Identify a machine: POST /v1/identify
  3. Send data: POST /v1/normalize
  4. Connect a tool: POST /v1/tools
  5. Set up automation: POST /v1/triggers/natural
  6. Query history: GET /v1/history/{machine_id}
  7. Tamper-evident record: POST /v1/settle

All /v1/* requests need Authorization: Bearer fnet_… and (where applicable) Content-Type: application/json.

Endpoints

POST /v1/identify

Provision or look up a persistent, verified identity (machine_id) for one machine. Idempotent on (oem, model, serial).

{
  "oem":    "fanuc",
  "model":  "Robodrill α-D21MiB5",
  "serial": "R30iB-12345",
  "site":   "Line 3"
}
{
  "machine_id":     "FORGE-4de03b",
  "internal_id": "fanuc:m-20ia:r30ib-12345",
  "created":     true,
  "machine":     { "status": "active" },
  "first_seen":  "2026-05-04T01:00:00Z"
}
POST /v1/normalize

Translate raw OEM telemetry to canonical FCS data. Auto-provisions identity from oem/model/serial when no machine_id is given. Returns triggers_fired[] when any active trigger matches. Optional idempotency_key dedups retries within 24h. Optional observed_at (ISO 8601 controller clock) is stored alongside the server ingested_at and used for sustained-trigger windows.

{
  "data":   { "Spindle_Speed": 2200, "spindle_load": 92 },
  "oem":    "fanuc",
  "model":  "Robodrill α-D21MiB5",
  "serial": "R30iB-12345",
  "observed_at":     "2026-05-05T09:30:15Z",
  "idempotency_key": "optional-caller-supplied-key"
}
{
  "normalized":      { "spindle_speed_rpm": 2200, "spindle_load_pct": 92 },
  "field_mappings":  { "spindle_load": { "mapping_id": "6c34…", "canonical": "spindle_load_pct", "confidence": 0.93 } },
  "fields_total":    2,
  "fields_renamed":  2, // source != canonical
  "fields_identity": 0, // source == canonical
  "fields_unknown":  0, // no mapping
  "coverage_pct":    100,
  "machine_id":      "FORGE-4de03b",
  "history_id":      "c419…",
  "idempotency_key": "ab12…",
  "observed_at":     "2026-05-05T09:30:15+00:00",
  "ingested_at":     "2026-05-05T09:30:18+00:00",
  "deduplicated":    false,
  "triggers_fired":  [{ "trigger_id": "7f0c…", "trigger_name": "high spindle load", "actual_value": 92 }]
}

If a retry hits the same idempotency_key within 24h, response includes deduplicated: true + the original history_id, and trigger evaluation is skipped. If |observed_at − ingested_at| > 60s, response includes clock_skew_warning.

POST /v1/tools

Register a webhook endpoint as a tool. Triggers reference these by tool_id. auth_secret returned ONCE on create; never read back via GET.

{
  "name": "slack-maintenance",
  "url":  "https://hooks.slack.com/services/T…/B…/xxx",
  "method": "POST",
  "payload_template": { "text": "Alert: {{oem}} {{model}}, {{field}} at {{value}}" },
  "auth_type": "none"
}
{ "id": "add2…", "name": "slack-maintenance", "auth_type": "none", "enabled": true }
POST /v1/triggers/natural

Parse a natural-language instruction into a structured trigger. NEVER auto-activates; returns parsed_trigger for review and explicit POST to /v1/triggers.

{ "machine_id": "FORGE-4de03b", "instruction": "Alert maintenance Slack when spindle load exceeds 90." }
{
  "parsed_trigger": {
    "machine_id": "FORGE-4de03b",
    "name": "Spindle Load Alert",
    "condition": { "field": "spindle_load_pct", "op": ">", "value": 90 },
    "actions":   [{ "tool_id": "add2…" }]
  },
  "confirmation_required": true
}
GET /v1/history/{machine_id}

Per-machine canonical history. Query params: from, to (ISO-8601), fields (csv), limit (≤1000), summary (bool).

GET /v1/history/FORGE-4de03b?limit=5&summary=true
{
  "machine_id":  "FORGE-4de03b",
  "machine":  { "oem": "fanuc", "model": "Robodrill α-D21MiB5", "site": "Line 3" },
  "row_count": 2,
  "avg_coverage": 100,
  "fields_covered": ["spindle_load_pct", "spindle_speed_rpm", "sensor_readings.coolant_temp"]
}
POST /v1/settle

Create a tamper-evident, independently auditable record. Every event is hash-chained into the machine's verified history. Add ?batch=true with body {"machine_id": "…"} to roll all unsettled events for that machine into one record.

POST /v1/settle?batch=true
{ "machine_id": "FORGE-4de03b" }
{
  "machine_id":   "FORGE-4de03b",
  "record_hash":  "413d5ec…",
  "event_count":  4,
  "chain_index":  128,
  "verify_url":   "https://forge.foundrynet.io/v1/attestation/FORGE-4de03b"
}

The verified history is exportable for lessors, insurers, and auditors. Optional off-network anchoring is available on Enterprise.

MCP server

Connect any MCP-compatible client (Claude, Cursor, custom agents, LangChain, CrewAI, AutoGen). One line:

claude mcp add --transport http forge https://mcp.foundrynet.io/mcp --header 'Authorization: Bearer YOUR_API_KEY'

Endpoint: https://mcp.foundrynet.io/mcp (Streamable HTTP). Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json):

{
  "mcpServers": {
    "forge": {
      "command": "npx",
      "args": ["mcp-remote", "https://mcp.foundrynet.io/mcp"]
    }
  }
}

Listed on Smithery, Glama, and the MCP directory.

MCP tools (29 total)

Plus operational capability tools, calculate_oee, energy_report, shift_report, diagnose, and parts_prediction, for 29 tools total. All run on the same normalized data layer. Works with any MCP-compatible client.

Prediction input contract

The forecasting tools, predict, predict_breach, and remaining_life, take the canonical series as a time_series array (not values), ordered oldest to newest, and require a minimum of 16 data points. The canonical_field must be a recognized canonical, call get_coverage to list them.

POST /v1/predict_breach
{
  "canonical_field": "spindle_load_pct",
  "time_series": [60, 61, 62, 63, 64, 66, 67, 68, 70, 71, 73, 74, 76, 78, 80, 82],
  "threshold": 95,
  "direction": "above",
  "horizon": 24
}

fleet_health and predict_batch take a fleet payload and are POST, not GET. Fewer than 16 points, or an unrecognized canonical field, returns the 422 envelope shown below.

Reliability + record integrity

Soft delete

DELETE /v1/triggers/{id} defaults to soft delete, sets deleted_at, hides from list/eval, restorable for 30 days via PATCH /v1/triggers/{id}/restore. Add ?permanent=true for irrecoverable hard delete.

Idempotency

/v1/normalize accepts an optional idempotency_key. If absent, the server synthesizes sha256(machine_id + canonical(data))[:32]. Repeats within 24h return the original response with deduplicated: true and skip trigger evaluation. Telemetry gateways: set this on retry.

Coverage metric

Every /v1/normalize response carries three counters: fields_renamed (source != canonical), fields_identity (source == canonical), fields_unknown (no mapping). coverage_pct = (renamed + identity) / total. Identity mappings, like parts_count → parts_count at confidence 1.0, count toward coverage.

Record timestamps

observed_at (ISO 8601 from the controller clock) is optional on /v1/normalize. Stored alongside ingested_at (server). When the difference exceeds 60s, the response includes clock_skew_warning. Sustained-condition trigger windows reference observed_at when present so windows are anchored to when events actually happened, not when they arrived.

Structured errors

Every /v1/* validation failure returns the same envelope:

{
  "error":  true,
  "status": 422,
  "field":  "body.oem",
  "reason": "Field required",
  "received_value": {},
  "suggestion":     "e.g. 'fanuc'",
  "additional_errors": [ { "field": "body.model", "reason": "Field required" } ]
}

System requirements

The Forge kernel runs as a Docker container on any Linux host: cloud, on-premise server, industrial PC, or edge gateway.

Minimum (edge deployment)

CPU:      2 cores, x86_64 or ARM64
RAM:      4 GB
Storage:  10 GB available
OS:       Linux (Ubuntu 20.04+, Debian 11+, Amazon Linux 2)
Network:  Ethernet to OT network (Modbus / OPC UA / SNMP / MTConnect / EtherNet/IP / PROFINET / BACnet / ROS2 / J1939 / MAVLink)
          Optional internet for cloud sync and updates
Runtime:  Docker 20.10+ or AWS Greengrass V2

Recommended (production)

CPU:      4 cores
RAM:      8 GB
Storage:  50 GB (accommodates extended telemetry history)
Network:  1 Gbps to OT network + internet for CMMS and updates

Cloud deployment

Any container host: Railway, AWS ECS, GCP Cloud Run, Azure Container Apps. No special hardware. Connects to equipment via API or protocol bridge.

Resource profile

Container image:    ~2.2 GB (compressed ~900 MB)
Runtime memory:     ~1.1 GB RSS steady state, ~1.5 GB peak
CPU at idle:        < 5% single core
CPU during predict: ~40% single core for 2-3 seconds
Bandwidth:          < 1 MB/hour per machine at 1 reading/second

What is in the 1.1 GB

Normalization engine (BGE-M3 embeddings):  ~600 MB
Prediction engine (TimesFM):                ~400 MB
Application + vertical packs + schema:      ~100 MB

Supported protocols

MQTT / Sparkplug B    (push, real-time streaming)
Modbus RTU / TCP      (poll, legacy PLCs and sensors)
OPC UA                (subscribe, modern controllers)
SNMP                  (poll, servers and network equipment)
MTConnect             (poll, CNC controllers: Haas, Mazak, Okuma, DMG Mori)
EtherNet/IP (CIP)     (poll, Allen-Bradley / Rockwell PLCs)
PROFINET / S7         (poll, Siemens S7-300/400/1200/1500)
BACnet/IP             (poll, building automation: BMS, HVAC, meters)
ROS2 / DDS            (subscribe, robots and AMR fleets)
J1939 / CAN           (listen, engines and heavy equipment)
MAVLink               (listen, drones and autonomous vehicles)
REST API              (push or pull, any HTTP-capable device)
CSV upload            (batch, historical data import)

No new hardware required

Forge connects to your existing sensors, PLCs, and controllers through the protocols they already speak. No proprietary gateways, no sensor replacements, no wiring changes.

Pricing

Metered billing via Stripe. Counts roll up to your subscription's current billing period and settle on Stripe's normal cycle. Query GET /v1/usage for live counts and an estimated bill.

Get an API key via POST /v1/keys with a Stripe payment method (email + payment_method_id from Stripe.js).