Docs / Contact / sdks/javascript/getting-started

JavaScript SDK: Getting Started

Go from zero to publishing data in under a minute. The SDK uses the native fetch API with zero external dependencies and includes React hooks for instant component integration.

Requirements: Node.js 16+ or modern browser (Chrome 88+, Firefox 84+, Safari 14+, Edge 88+)

Install#

The SDK installs straight from GitHub:

bash

npm install github:tendrl-inc-labs/contact-js

It installs under its package name @tendrl/contact — the imports below work unchanged. (The SDK is plain JavaScript with no build step, so the GitHub install ships exactly what a registry install would.)

Need an API key?

Every client needs an entity API key. If you haven't created one yet, create your first entity and copy its API key from the Connection Instructions dialog.

Your First Message (4 Lines)#

javascript


const client = new TendrlClient({ apiKey: 'your_api_key' });
client.start();
client.publish({ temperature: 23.5, humidity: 60 }, ['sensor']);

The message is queued, batched, and delivered. Tags route it to your flows and connectors.

Pointing at a different server#

By default the client talks to https://app.tendrl.com. Set TENDRL_APP_URL to point it at a local development stack instead:

bash

TENDRL_APP_URL=http://localhost:8000 node app.js

It accepts either a bare origin (http://localhost:8000) or a full base URL ending in /api, so the same variable works for the Python, Go, and nano-agent clients too. In a browser, where there is no process.env, pass apiBaseUrl to the constructor instead. See Testing against a local or staging stack.

React: One Hook, Full Integration#

The useTendrlClient hook gives you a connected client and publish/receive functions, managed automatically with the component lifecycle:

javascript


function SensorDashboard() {
    const [online, setOnline] = useState(false);

    const { client, publish } = useTendrlClient({
        onMessage: (msg) => console.log('Command:', msg.data),
        offlineStorage: true
    });

    useEffect(() => {
        if (!client) return;
        const id = setInterval(async () => {
            setOnline(await client.checkConnectionState());
        }, 5000);
        return () => clearInterval(id);
    }, [client]);

    return (
        <div>
            <p>Status: {online ? 'Online' : 'Offline'}</p>
            <button onClick={() => publish({ temperature: 23.5 }, ['sensor'])}>
                Send Reading
            </button>
    );
}

The hook starts the client on mount and stops it on unmount. No cleanup code to write.

The API key is only read from the environment — the hook has no apiKey option, and passing one is ignored. Set it before starting your dev server or build:

bash

REACT_APP_TENDRL_KEY=your_api_key

With that variable unset, the hook logs an error to the console and creates no client at all, so client stays null and publish is a no-op.

Why not the hook's own `isConnected`?

It is read straight off the client instance during render, not held in React state, so nothing re-renders when connectivity changes — and it is false on the first render, because the client is created in an effect that runs afterwards. Polling client.checkConnectionState() into useState, as above, is what actually updates the UI.

Receive Inbound Messages#

Handle messages sent back from Contact flows. There is one receive mechanism: a single callback, set either on the constructor or with setMessageCallback. There is no client.on() router in this SDK — branch on the message's tags yourself.

javascript

client.setMessageCallback((message) => {
    // The SDK moves the server's tags into message.context.tags
    const tags = message.context?.tags || [];
    if (tags.includes('diagnostic')) {
        client.publish({ self_test: 'pass' }, ['diagnostic-result']);
    } else if (tags.includes('ai-response')) {
        console.log('AI:', message.data?.response);
    } else if (tags.includes('alert') || tags.includes('anomaly')) {
        console.log('Alert:', message.data);
    }
});

client.setMessageCheckRate(3000);

Polling only runs while a callback is set. Setting one on a running client starts polling immediately; setMessageCheckRate and setMessageCheckLimit re-arm it with the new values.

Track Device State#

Every entity has a persistent state table, but the JavaScript SDK does not wrap it. There is no getState, updateState, replaceState or onState. Call the REST endpoints with the entity's API key:

javascript

const appUrl = process.env.TENDRL_APP_URL || 'https://app.tendrl.com';
const base = `${appUrl}/api/entities/status-table`;
const headers = {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
};

// Read current state
const state = await (await fetch(base, { headers })).json();

// Merge new keys (PATCH)
await fetch(base, {
    method: 'PATCH',
    headers,
    body: JSON.stringify({ firmware: '2.1.0', status: 'active' }),
});

// Replace the whole table (PUT)
await fetch(base, {
    method: 'PUT',
    headers,
    body: JSON.stringify({ firmware: '2.1.0' }),
});

See State Table for the full endpoint reference.

Offline Storage with Zero Configuration#

Enable IndexedDB persistence with one flag. Messages queue locally when the network drops and send automatically when it returns:

javascript

const client = new TendrlClient({
    apiKey: 'your_key',
    offlineStorage: true
});

No retry logic to write. No local database to manage. The SDK handles storage, TTL expiration, and batch replay.

What You Get for Free#

When you call client.start(), the SDK automatically:

Without offlineStorage, a batch that fails to send — or a message that arrives when the queue is already full — is discarded, and nothing is logged unless you also pass debug: true. Turn both on while you are getting a client working.

Complete Example#

javascript


const client = new TendrlClient({
    apiKey: 'your_api_key',
    offlineStorage: true
});

client.setMessageCallback((msg) => {
    if ((msg.context?.tags || []).includes('ai-response')) {
        console.log(`[${msg.msg_type}] ${JSON.stringify(msg.data)}`);
    }
});
client.setMessageCheckRate(5000);
client.start();

// Publish sensor data every 10 seconds
setInterval(() => {
    client.publish(
        { temperature: 23.5, humidity: 60 },
        ['sensor', 'environment']
    );
}, 10000);

What's Next#