Operations & troubleshooting
Running the sync day to day: the heartbeat, disconnecting, why signed requests don't blindly retry, and how to run it on a schedule.
Heartbeat — status()
const status = await client.status();
console.log(status.providerStatus, status.entityCounts, status.lastSession);
A signed GET that bumps the connection pulse and returns the connection status, per-kind entity
counts, and the last sync session summary (StatusResult). Throttle to at most once every ~5
minutes — it's a heartbeat, not a poll loop.
Disconnect — unpair()
await client.unpair();
Tells the backend to disconnect, then wipes the locally stored credentials — even if the remote call fails, so you can always re-pair. Run it before pairing a different tenant.
Retry semantics
This is the rule that trips people up: signed requests are never auto-retried on 429/5xx.
| Path | Signed? | Retry policy |
|---|---|---|
pair | no | retries on 429/5xx/connection (no nonce spent) |
snapshot upload (PUT) | no (single-use URL) | not retried |
init / finalize / sendEvents / status / unpair | yes | no auto-retry; opt into resignRetries |
A signed request carries a single-use nonce the backend consumes the moment it arrives — before it can return a 429/5xx. A blind retry would replay a spent nonce and be rejected as a replay. So signed calls retry on connection errors only. To resend an idempotent signed call after a transient error, opt into a resign-and-resend — each retry mints a fresh nonce:
await client.sendEvents(ops, { resignRetries: 2 });
You can set a default resignRetries on the client constructor. Because every /events batch carries a
stable batch_id (its Idempotency-Key), a resend of the same batch replays rather than double-applies.
Logic errors don't throw
A 429/5xx on a signed call surfaces as a thrown TransientError (it's a transport failure). But
per-op logic errors (a missing required field, a bad slug, money as a float) come back inside
EventsResult.errors and do not throw — read them and fix the data. See
Syncing → Reading the result.
426 Upgrade Required
The backend requires a newer sync protocol than this package speaks. There is no fallback — update the package:
npm install @monoverse/voicebot-node@latest
Running on a schedule
You own the cadence — the proven pattern is frequent deltas plus one periodic full. A minimal cron-driven worker:
import { client } from './voicebot';
import { collectChangedOps, fullCatalog } from './catalog';
async function deltaRun() {
const ops = await collectChangedOps(); // since your last watermark
if (ops.length) await client.sendEvents(ops, { resignRetries: 2 });
}
async function fullRun() {
await client.snapshot(fullCatalog());
}
// Wire these to your scheduler:
// */15 * * * * node sync-worker.js delta
// 0 3 * * * node sync-worker.js full
const mode = process.argv[2];
await (mode === 'full' ? fullRun() : deltaRun());
- High-churn store → delta every 5 minutes.
- Typical store → delta every 15 minutes (above).
- Schedule the full for a low-traffic hour so the larger upload doesn't compete with peak load.
A long sync that runs past its next tick must not start a second copy. Guard with your scheduler's overlap protection (or a lock), and prefer dispatching the full to a dedicated worker.
Long-running process? Use CatalogScheduler
If your app is a daemon (an Express/Nest process, a container that stays up), the package ships a
scheduler so you don't hand-code the loop — interval timers, a without-overlapping guard, and
onError / onSync callbacks that keep the process alive through a failed run:
import { CatalogScheduler } from '@monoverse/voicebot-node';
const scheduler = new CatalogScheduler({
full: () => client.snapshot(fullCatalog()),
delta: () => collectChangedOps().then((ops) => (ops.length ? client.sendEvents(ops, { resignRetries: 2 }) : undefined)),
runFullOnStart: true,
});
scheduler.start(); // stop() on shutdown
Defaults: delta every 15 minutes, full every 24 hours (deltaIntervalMs / fullIntervalMs to
change).
The cabinet's Synchronize button doesn't reach a producer
The Synchronize button in the VoiceBot cabinet works by calling into your store over HTTP — it
can only reach integrations that expose an inbound sync hook (the WordPress plugin, the Laravel
package). A Node producer has no inbound endpoint, so that button does not apply to this stack:
your cron/CatalogScheduler cadence is the trigger. To push immediately, run your own sync worker;
the cabinet's sync coverage panel reflects the result either way.
Encrypted credential storage
The shared secret is stored encrypted at rest (AES-256-GCM, node:crypto) and is never logged,
printed, or serialized — Credentials redacts it from JSON.stringify, console.log, and
toString.
- Pass a full-entropy 32-byte
encryptionKey(openssl rand -hex 32); keep it in your secret manager / env. credentialsFilewrites the AES-GCM envelope to disk with0600perms; the plaintext secret never touches disk.- Omit
credentialsFilefor an in-memory store — the secret lives encrypted even in process memory. - For a KMS-backed at-rest key, implement
CredentialStoreand pass it ascredentialStore.
If you change encryptionKey, the stored envelope can no longer be decrypted and the client surfaces a
ConfigError ("re-pair or check VOICEBOT_ENCRYPTION_KEY"). Re-pair after a key rotation.
Troubleshooting
| Symptom | Cause & fix |
|---|---|
ConfigError: …must use https:// | baseUrl/ingest URL is plaintext http://. Switch to https:// (localhost excepted). |
refusing to push an empty full snapshot | Your entity source yielded zero rows. Fix the source; never force-push an empty snapshot — it would wipe the catalog. |
NotPairedError | No stored credentials. Call pair() first, or check that encryptionKey/credentialsFile match the run that paired. |
426 protocol_upgrade_required | Update the package — there is no protocol fallback. |
Per-op errors in the result | Validation/backend logic error. Common causes: missing required key, money as a float, category referenced by id instead of slug. |
ConfigError on credential load | Wrong/rotated encryptionKey. Re-pair. |
Next
- Ingest Protocol Reference → Errors — the full status table and the no-retry-on-signed rationale.