Skip to content

Five ways to describe your API. One single model out.

MCP server, OpenAPI, GraphQL, ready-made connector or manual entry: everything ends up in the same catalogue, and nothing is exposed until it is ticked.

Example
# Report an event: Nodium checks and files it, sending nothing yetcurl https://nodium.io/api/v1/templates/outbound \ -H "Authorization: Bearer nod_••••••••" \ -H "Content-Type: application/json" \ -d '{ "externalId": "order-2481-shipped", "eventType": "order.shipped", "phone": "+33 6 00 00 00 00", "templateName": "order_shipped", "payload": { "reference": "2481" } }'
200 · scheduled
{ "event": { "externalId": "order-2481-shipped", "eventType": "order.shipped", "status": "scheduled", "skipReason": null, "estimatedCostUsd": null }, "duplicate": false, "caller": "machine", "skipReasons": []}

The integration journey

Six moves, from the key to the first event. None makes visible what nobody has ticked.

  1. Create an API key

    In the console: Account › Advanced › API keys. The key is shown only once, acts on behalf of its creator and carries a role capped by theirs.

    Authorization: Bearer nod_…
  2. Connect your software

    Pick one of the five sources and drop in its authentication: it is encrypted and never comes back out of a read.

  3. Test the connection

    A failure comes back with a readable diagnosis, not an error. An address leading to a private network is refused.

  4. Describe the catalogue

    Automatic for an MCP server and an OpenAPI description, by hand for the others: objects, fields, operations.

    POST /api/v1/connectors/:id/discover
  5. Tick what is allowed

    Discovery writes everything as no. Your customer confirms; a write is born switched off and requires a human's approval.

  6. Report your events

    Your software announces a fact; Nodium checks the contact, their consent, the template and the channel, estimates the cost and files it. Your webhooks keep you informed.

    POST /api/v1/templates/outbound

The five sources

What each one asks for, and what it can do on its own.

  • Ready-made connector

    Your Shopify shop domain and a token.

    Already mappedThe fastest
  • MCP server

    The address of your MCP endpoint.

    Automatic discoveryThe most faithful
  • OpenAPI description

    The address of the OpenAPI file itself, not of the documentation page.

    Automatic discoveryThe long tail
  • GraphQL schema

    The address of the endpoint, the one that receives the queries.

    Catalogue by handRequired for Shopify
  • Manual entry

    Nothing to publish: your objects are described one line per field.

    Catalogue by handNo API

Why we start with MCP

  1. Because tool descriptions carry meaning and not only shape. That is the weakness of an OpenAPI description: it says how to call, without saying what the call means.
  2. Because the input schema of an MCP tool is already JSON Schema, so exactly our format. It is copied across as is: no conversion, therefore no loss.
  3. Because the tool list is a living discovery. You add a tool, a new discovery adds it — without ever unticking what your customer had confirmed, or renaming what they had renamed.
Risk annotations decide nothing.A tool that declares itself harmless but whose name says cancel or delete is still treated as a write. A verb we do not know is treated as a write. An annotation pre-sorts the list; it never switches anything on by itself.

Discovery proposes, your customer confirms. Everything found is written as no: no field is visible, no operation is callable until it has been ticked. The screen arrives pre-ticked on a handful of fields, writing off, and one can move on without touching anything.

  • The field that serves as the reference, and those that identify an end customer.
  • Sensitive fields, never proposed as exposed, and masked in the logs.
  • The operations allowed, and those that require a human to approve.
  • A read that became a write at the next discovery is switched off outright: nobody had accepted that one.

Three families of call

At run time, everything comes down to these three.

  1. search

    Search

    Find the object the conversation is about, from a phone number, an email address or a reference.

  2. read

    Re-read

    Read an already attached object again, so the answer is about its current state and not a memory of it.

  3. execute

    Execute

    Act in your software. Switched on operation by operation, with a mandatory deduplication key: a replayed call never charges twice.

    Off by default

Calling Nodium from your software

Your system has no session: it identifies itself with an API key and reports an event — an order shipped, a deadline coming up. Nodium checks the contact, their consent, the existence of an approved template, the channel, and then estimates the cost. That call sends nothing: it decides.

  • The same event twice goes out once: the call is idempotent on the identifier you supply, and replays the original decision.
  • A key that is presented and wrong is refused on the spot. Never a quiet fallback to a session: an expired key must be visible immediately.
  • A refusal states everything that was missing, not just the first reason.
  • The cost is estimated before sending. A price we do not know is called unknown, never free.
Example
# HTTP 200: the event is filed, nothing is sent{ "event": { "externalId": "order-2482-shipped", "status": "skipped", "skipReason": "no_consent" }, "duplicate": false, "caller": "machine", "skipReasons": ["no_consent", "template_not_approved"]}

Staying informed: webhooks

A subscription, created in the console or through the API, receives the events it chose as POST requests. Every delivery is signed; answer with a 2xx code.

The events

  • message.received
  • message.sent
  • approval.requested
  • approval.decided
  • mission.closed
  • dossier.changed
  • timer.fired

Without a 2xx answer, the delivery is retried: six attempts at most, after 1 min, 5 min, 30 min, 2 h and then 12 h.

The signing secret is returned only once, when the subscription is created. The signature carries the sending time and an HMAC-SHA256 of the time and the body.

Example
import { createHmac, timingSafeEqual } from 'node:crypto'// Reject any delivery whose signature does not matchexport function isFromNodium(header, rawBody, secret) { const sig = Object.fromEntries(header.split(',').map(p => p.split('='))) const expected = createHmac('sha256', secret) .update(`${sig.t}.${rawBody}`) .digest('hex') return sig.v1?.length === expected.length && timingSafeEqual(Buffer.from(sig.v1), Buffer.from(expected))}

Quick reference

What holds for every call, in both directions.

Calling the Nodium API

Address
https://nodium.io/api/v1/…A route that changes shape will move to v2 without breaking v1.
Authentication
Authorization: Bearer nod_…A key acts on behalf of its creator, with a role capped by theirs; everything is logged in their name.
Isolation
The key designates its workspace. A resource from another workspace answers 404, never 403.
Rate
600 calls per minute per key; beyond that, 429 with the time to wait.
Idempotency
An event replays on its externalId, a message send on its idempotencyKey: the same key returns the same result.
Errors
JSON with statusCode and statusMessage: a message in French that can be displayed as is, and a code when a program must react differently.
Pagination
By cursor, never by page number: a queue moves under your fingers.

When Nodium calls your API

Five ways to authenticate, the ones APIs actually use: no authentication, a token presented as an Authorization Bearer, a key in a header you name, a username and password pair, or an OAuth 2 token pasted by hand.

OAuth 2 is not yet delegated authorisation.An OAuth 2 token is presented as an ordinary token and is not refreshed: when it expires, it has to be replaced. There is no Authorise Nodium button, and we would rather write that here than let you find out in production.

What we do with your secrets

  • The authentication secret is encrypted and never comes back out. A read only says whether there is one.
  • A key placed in a field that was not meant for it is refused, together with the place where it belongs.
  • Nothing crosses the wall between workspaces. An identifier belonging to someone else answers not found, never forbidden.
  • An address pointing at a private network is refused when the connection is tested.
  • Fields marked as sensitive are masked in the call logs.

What we cannot do yet

The list is short and kept current. A limit written down costs less than a limit discovered.

  • Automatic discovery only exists for MCP servers and OpenAPI descriptions. A GraphQL schema can be tested, but its catalogue is described by hand.
  • The older two-address MCP transport is not supported.
  • No OAuth 2 token renewal.
  • Every MCP call reopens a handshake: slightly slower, but nothing to repair when your server restarts.
  • Sensitive data sitting in a field nobody marked as such is logged in clear. Discovery proposes the marking, your customer completes it.

Connect your API

Register a source, test it, then discover its catalogue — three moves, from one screen.