Operations & troubleshooting
Running the sync day to day: the commands, their exit codes, what cron should alert on, and how the package recovers from failures without losing data.
voicebot:sync
php artisan voicebot:sync # incremental delta (the cron path)
php artisan voicebot:sync --full # complete snapshot (first run, then nightly)
php artisan voicebot:sync --dry-run # compute + print what WOULD be sent; push nothing
php artisan voicebot:sync --since="2026-06-01" # delta from a specific instant (catch-up)
php artisan voicebot:sync --queue # dispatch to a queue worker and return immediately
| Flag | Effect |
|---|---|
--full | Stream a full snapshot instead of a delta. Cannot be combined with --since. |
--since=<datetime> | Delta only — override the stored watermark (any strtotime/ISO-8601 value). |
--dry-run | Compute and print counts; push nothing. Cannot be combined with --queue. |
--queue | Dispatch the sync to a worker (queue from VOICEBOT_SYNC_QUEUE) and return. |
Full snapshot
php artisan voicebot:sync --full
Streams a gzip-NDJSON snapshot of every enabled kind, opens an upload session, PUTs the file, and finalizes. On success the per-kind watermark is advanced to the snapshot start, so the next delta only carries later changes.
Full snapshot: 1298 records.
product 1240
category 58
sync_id: 7b2e…
Delta
php artisan voicebot:sync
Per kind, streams rows changed since the watermark as /events batches (capped by op count and bytes),
then advances the watermark only if every batch for that kind succeeded.
Delta: 37 ops in 1 batch(es).
Queueing large syncs
--queue dispatches the work to a queue worker and returns immediately — ideal under
->withoutOverlapping() so the scheduled tick doesn't block. Requires a running queue worker. It cannot
be combined with --dry-run.
Exit codes
The command's exit code is cron-friendly — wire your monitor to it.
| Code | Constant | Meaning | What to do |
|---|---|---|---|
0 | success | Sync completed (or --dry-run finished). | Nothing. |
1 | transient | Network/5xx failure, or a delta that dead-lettered ops. | Alert. Retry next run. |
2 | config | Not paired, empty snapshot refused, bad model, bad --since. | Fix config. A retry won't help. |
Alert on 1 (transient / partial failure) and 2 (config error). 2 means the next scheduled
run will fail the same way until you fix the configuration — page on it. 1 is usually self-healing on
the next run, but a sustained streak of 1 (e.g. dead-letters piling up) deserves attention.
A delta that parked any ops in the dead-letter table exits 1 even though the rest pushed — it's a
partial failure, surfaced so your monitor notices.
voicebot:doctor
php artisan voicebot:doctor
php artisan voicebot:doctor --samples=3 # inspect more sample rows per kind
A read-only pre-flight: verifies pairing, backend reachability, and — per enabled kind — a resolvable
source, a non-empty map, and the backend-required payload keys on a sampled mapped row. Emits one sample
record per kind and pushes nothing. Exits 0 on all-clear, 1 if anything failed. Run it after any
mapping change. (Output example in Installation.)
voicebot:unpair
php artisan voicebot:unpair
Tells the backend to disconnect, then wipes the locally stored tenant id + shared secret. The local
credentials are cleared even if the remote call fails (it exits 1 in that case), so you can always
re-pair. Run this before pairing a different tenant.
Reliability model
Watermark / delta semantics
The watermark is per entity kind (voicebot_sync_state). A delta selects rows whose updated_at is
strictly greater than the stored watermark, then advances the watermark to the instant the run
started — not to the max updated_at of the rows sent.
Why run-start: a row written during the run carries an updated_at later than the start instant.
Advancing to max-updated_at would skip it forever; advancing to run-start re-includes it next run.
That's at-least-once delivery, made safe by the fact that upserts are idempotent. The watermark only
advances when every batch for the kind succeeded — a failed kind's window is retried in full next run.
--since overrides the stored watermark for a one-off catch-up (it does not change the stored value).
Dead-letter table & bounded attempts
A delta batch that fails permanently (a non-retryable backend error) is never silently dropped — its
ops are parked in voicebot_dead_letter. Re-failures of the same op identity
(entity_kind + external_id + op_type) bump an attempts counter instead of duplicating rows. Once
attempts reaches sync.dead_letter_max_attempts (default 5, env
VOICEBOT_DEAD_LETTER_MAX_ATTEMPTS) the row is flagged exhausted for manual inspection/replay rather
than being re-parked forever.
Inspect parked ops directly:
SELECT entity_kind, external_id, op_type, attempts, exhausted, error, failed_at
FROM voicebot_dead_letter
ORDER BY failed_at DESC;
A non-empty, growing dead-letter table means a delta keeps failing — read error, fix the mapping or
the data, and the next sync that re-touches those rows will re-attempt them.
Why signed calls don't retry on 5xx
Signed requests carry a single-use nonce that the backend consumes the moment it receives the
request — 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; the unsigned pair handshake and the no-HMAC
snapshot upload do retry on 429/5xx with backoff that honours Retry-After. This is why a
transient backend error on a delta dead-letters the batch (exit 1) rather than retrying inline.
Troubleshooting
Not paired. Run voicebot:pair <code> first. (exit 2)
No stored connection. Run php artisan voicebot:pair VB-XXXX-XXXX (see
Installation). If you just switched tenants, you must unpair then pair.
refusing to push an empty full snapshot (exit 2)
A --full produced zero rows across all enabled kinds. Pushing it would arm the server tombstone
guard to wipe the catalog, so it's refused. Almost always a mapping/config error: a wrong model, an
enabled kind with no data, or a query that returns nothing. Run voicebot:doctor — a kind showing
0 rows is the culprit.
A full snapshot tells the backend "this is everything" — so the server tombstones anything not in it. An accidental empty snapshot would therefore wipe the catalog. The client refuses to send one; never work around this by forcing the push — fix the mapping so the snapshot has rows.
--since is not a valid date/time (exit 2)
The --since value didn't parse. Use ISO-8601 (2026-06-01 or 2026-06-01T12:00:00Z) or any value
strtotime accepts.
VoiceBot base/ingest URL must use https:// (exit 2)
The configured URL is plaintext http://. An HMAC over http would leak the signature; switch to
https://. localhost / 127.0.0.1 / ::1 are the only exceptions (local dev).
protocol_upgrade_required (HTTP 426)
The backend requires a newer sync protocol version than this package speaks. Upgrade the package — there is no fallback; older protocol versions are refused outright:
composer update monoverse/voicebot-laravel-sync
The sync protocol is versioned and the backend rejects out-of-date producers with HTTP 426 rather
than silently degrading. If a deploy starts failing with protocol_upgrade_required, the fix is always
to update the Composer package; there is no compatibility shim.
Delta reports dead-lettered ops (exit 1)
Some ops failed validation or hit a backend error and were parked. Query voicebot_dead_letter, read
each error, and fix the mapping or data. Common causes: a required payload key is missing/empty (see
Data reference), money sent as a float instead of minor units, or a
category referenced by id instead of slug.
Backend unreachable or rejected in doctor
Check VOICEBOT_BASE_URL and network egress from the app server. If pairing is fine but status fails,
the connection may have been disabled in the cabinet (the billing kill-switch revokes ingest when the
provider connection isn't connected).
Sample row missing required payload key(s) in doctor
The mapped record for that kind doesn't carry the backend-required field (e.g. product without name,
page without title). Add the missing key to the kind's map. The exact requirement per kind is in
Data reference.