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 foundrynet.io/quickstart, issues a forge_sandbox_ key instantly, no card
  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. reproducible and verifiable against the corpus version record: POST /v1/settle

All /v1/* requests need Authorization: Bearer <key> and (where applicable) Content-Type: application/json. There are two key tiers, and the same key behaves identically on REST and MCP:

Agent keys (forge_agent_…) inherit the limits of the account that created them: an agent under a sandbox account gets the free allowance; under a paid account it is metered.

Limits

Two separate limits apply, and they return different errors:

Normalize latency scales with how many of your tags are new to the corpus: unfamiliar tags take the vector and LLM paths on their first sighting and are cached afterwards, so the first call from a new plant is the slowest one it will ever make.

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":     "machine_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":      "machine_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": "machine_4de03b", "instruction": "Alert maintenance Slack when spindle load exceeds 90." }
{
  "parsed_trigger": {
    "machine_id": "machine_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/machine_4de03b?limit=5&summary=true
{
  "machine_id":  "machine_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 reproducible and verifiable against the corpus version, 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": "machine_4de03b" }
{
  "machine_id":   "machine_4de03b",
  "merkle_root":  "d35ae2e…",
  "event_count":  4,
  "event_types":  { "identify": 1, "normalize": 3 },
  "period_start": "2026-08-02T21:00:00Z",
  "period_end":   "2026-08-02T22:00:00Z",
  "settlement":   "anchored",
  "settled_at":   "2026-08-02T22:00:04Z"
}

Read the machine's verified history back with GET /v1/history/{machine_id}.

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 (30 total)

Plus operational capability tools, calculate_oee, fleet_oee, energy_consumption, shift_report, health_index, diagnose_machine, get_agent_card, list_agents, list_guardrails, and check_guardrail, for 32 tools total. All run on the same normalized data layer. Works with any MCP-compatible client.

Prediction input contract

The forecasting tools are stateless. They do not read your stored telemetry. Passing only a machine_id returns 422. You send the series you want forecast, in the request body, on every call. Forge stores your normalized history for health, OEE, and triggers, but a prediction never reads it, so you always know exactly which numbers a forecast was based on.

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 numeric data points. The canonical_field is optional; when supplied it must be a recognized canonical, call get_coverage to list them. machine_id is the machine's identifier; on a prediction it only labels the reproducible and verifiable against the corpus version integrity record and does not load data.

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
}

A real 16-point series read off a Prusa MK2 hotend as it cools, showing the same contract with live data:

curl -X POST https://forge.foundrynet.io/v1/predict_breach \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "canonical_field": "hotend_temperature_c",
    "time_series": [167.1, 162.0, 159.0, 155.8, 153.4, 150.7, 148.3, 146.0,
                    143.2, 140.8, 138.2, 136.1, 134.0, 131.8, 129.7, 127.9],
    "threshold": 100,
    "direction": "below",
    "horizon": 16
  }'

fleet_health and predict_batch take a fleet payload and are POST, not GET. fleet_health expects machines (each carrying its own values), not machine_ids, and is likewise stateless. Fewer than 16 points, an all-identical series (no variance), 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 TCP / OPC UA / 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

13 usable of 15 declared. This list is the same set GET /health reports, so the two cannot disagree; the unavailable two are named below with their blockers rather than omitted.

MQTT / Sparkplug B    (push, real-time streaming — see the note below)
Modbus TCP            (poll, legacy PLCs and sensors)
OPC UA                (subscribe, modern controllers)
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)
MachineMetrics        (poll, GraphQL — machines already on MachineMetrics)
REST API              (push or pull, any HTTP-capable device)
CSV upload            (batch, historical data import)

Declared but not available:

Modbus RTU            no serial transport is implemented; use Modbus TCP,
                      or a serial-to-TCP gateway in front of the device
SNMP                  the adapter is not wired

Sparkplug B, stated precisely: the transport works and topics are parsed, but Sparkplug's protobuf payloads are not decoded, so a device that publishes metrics only inside a protobuf body will connect and produce nothing usable. Plain MQTT payloads are unaffected.

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.

A Normalized Telemetry Event (NTE) is each individual tag-value pair submitted for resolution — a single request carrying N tag-value pairs is N NTEs. Normalize bills on graduated tiers per 1,000 NTE: the first 100M events each month bill at the first-tier rate no matter your total volume, and only the events above each threshold bill at the next rate. Full ladder at /pricing.

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