/meany keyThe business and scopes this key acts for. Use it as a connection test.
curl https://lyverto.com/api/v1/me -H "Authorization: Bearer lyv_YOUR_API_KEY"Connect the systems you already run. Send consignments and shipment updates in — in your own format if you like — and get every event out, by webhook or by polling.
Base URL: https://lyverto.com/api/v1
The Lyverto API connects the systems you already run — a TMS, a warehouse spreadsheet, a carrier feed, Zapier — to Lyverto. Lyverto does not replace them. It carries data in (consignments and shipment updates, in your own format if you like) and out (to your customers’ tracking pages, email and WhatsApp, to your partners, and back to your tools as events).
| Base URL | https://lyverto.com/api/v1 |
| Format | JSON over HTTPS. Times are ISO 8601 in UTC. |
| Authentication | Authorization: Bearer <API key> |
| Machine-readable | OpenAPI 3.1 · This page as Markdown |
tracking:write and tracking:read scopes. Copy it — it is shown once.curl https://lyverto.com/api/v1/me \
-H "Authorization: Bearer lyv_YOUR_API_KEY"{
"success": true,
"data": {
"organizationId": "<uuid>",
"scopes": ["tracking:write", "tracking:read"],
"keyId": "<uuid>"
}
}curl -X POST https://lyverto.com/api/v1/consignments \
-H "Authorization: Bearer lyv_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"reference": "LC-0142",
"customerName": "Ama Mensah",
"customerPhone": "+233 24 123 4567",
"consolidationRef": "MSKU1234567",
"destination": "Tema",
"publish": true
}'The customer now has a live tracking page (trackingUrl in the response) and, because you published it, has been notified.
Every request except GET /openapi.json needs an API key, sent as a bearer token (or in an X-Api-Key header). Keys start with lyv_ and act for one business — nothing in a request body can name another.
| Scope | Allows |
|---|---|
tracking:write | Create and update consignments, submit updates, run mappings, send carrier feeds |
tracking:read | Read consignments, their timelines and your mappings |
events:read | Read the event stream |
webhooks:manage | Subscribe and unsubscribe webhooks (used by Zapier and Make instant triggers) |
Successful responses wrap the result in data. Errors carry a human-readable error and a stable machine-readable reason.
{ "success": true, "data": { … } }Every response has a Request-Id header (req_…). It is recorded in your request log; quote it when you contact support.
Endpoints that take many items (/events, /ingest/{mapping}, /feeds/dcsa) answer one result per item, in order. One bad item never fails the batch: it comes back skipped with a reason, and the rest are written.
{
"success": true,
"data": [
{ "index": 0, "status": "proposed", "proposalId": "<uuid>" },
{ "index": 1, "status": "duplicate", "id": "<uuid>" },
{ "index": 2, "status": "skipped", "reason": "no consignment or container of yours matches the target" }
]
}| Status | Meaning |
|---|---|
created / updated | A consignment was written |
proposed | The update is in your Review queue, waiting for a person (or an automation) to approve it |
appended | The update was written to the timeline directly |
duplicate | This externalEventId was already received; nothing was written |
skipped | The item could not be used; reason says why |
Give every update an externalEventId (your own id for it). Sending the same id again is answered duplicate, so a retry after a timeout never records an event twice. Consignments are keyed by reference, so re-sending one updates it instead of creating a copy.
Send ISO 8601 (2026-10-20T08:00:00Z). Through a mapping, 20/10/2026 (day first) and Unix seconds or milliseconds are also understood.
Limits count per business, across all of its keys. The free plan is meant for real use: one system or a couple of zaps, every day.
| Plan | Requests / minute | Requests / day | API keys | Webhooks | Mappings | Items per batch |
|---|---|---|---|---|---|---|
| Free | 60 | 2,000 | 2 | 2 | 3 | 50 |
| Starter | 300 | 25,000 | 5 | 10 | 15 | 200 |
| Professional | 1,200 | 250,000 | 25 | 50 | 100 | 500 |
| Enterprise | 3,000 | 2,000,000 | 100 | 200 | 500 | 1,000 |
Every response tells you where you stand:
Request-Id: req_4f0c2b9e7a1d3c5e8f6a2b1c
RateLimit-Limit: 60
RateLimit-Remaining: 57
RateLimit-Reset: 42
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 1790640060
X-Quota-Limit: 2000
X-Quota-Remaining: 1873
X-Plan: freeOver a limit you get 429 with Retry-After (seconds) and a reason: rate_limited means slow down and retry; quota_exceeded means the day’s quota is used (it resets at 00:00 UTC).
{
"error": "Too many requests: your plan allows 60 per minute. Retry in 18s.",
"reason": "rate_limited",
"requestId": "req_…"
}Use case: your TMS or spreadsheet is where bookings start. Push each one to Lyverto so the customer gets a tracking page, and push changes as they happen.
POST /consignments creates a consignment, or updates the one with the same reference. Only the fields you send are changed — a sync that does not know the customer’s email never erases the one your team typed.
curl -X POST https://lyverto.com/api/v1/consignments \
-H "Authorization: Bearer lyv_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "reference": "LC-0142", "eta": "2026-10-24T00:00:00Z" }'Change the status with PATCH /consignments/{reference}. Moving a draft to published, in_transit or delivered publishes it first, and the customer is told once. A published consignment cannot go back to draft — its customer has already seen it.
curl -X PATCH https://lyverto.com/api/v1/consignments/LC-0142 \
-H "Authorization: Bearer lyv_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "status": "delivered" }'Use case: the container departed, a consignment is held at customs, the ETA moved. Tell Lyverto once and every affected customer can see it — with the reason and the next step, which is what they plan their cash, trucks and stock around.
curl -X POST https://lyverto.com/api/v1/events \
-H "Authorization: Bearer lyv_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": [{
"target": { "reference": "LC-0142" },
"externalEventId": "tms-88121",
"label": "Customs hold",
"code": "exception",
"reason": "Physical inspection requested on HS 8517",
"nextStep": "Clearance expected in 3–5 working days. No action needed from you.",
"location": "Tema",
"eta": "2026-10-24T00:00:00Z"
}]
}'By default ("mode": "propose") updates wait in your Review queue until a person approves them — or an automation you set up does. Use "mode": "append" when the update comes from your own system and should go straight onto the timeline.
code | Use for |
|---|---|
picked_up | Received at origin or collected |
in_transit | Moving: loaded, departed, arrived, discharged |
customs_clearance | Customs events and releases |
out_for_delivery | On the last leg |
delivered | Handed over |
exception | Holds, damage, delays |
Use case: your system already produces JSON in its own shape, and changing it is not an option. Build a mapping once, point your system at its URL, and send the JSON as it is.
{{status}} at {{port}}. Translate your own codes with a lookup (DEP=in_transit).https://lyverto.com/api/v1/ingest/warehouse-sheet.curl -X POST https://lyverto.com/api/v1/ingest/warehouse-sheet \
-H "Authorization: Bearer lyv_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"rows": [
{ "Ref No": "LC-0142", "Client": "Ama Mensah", "Container": "msku1234567", "ETA": "20/10/2026" },
{ "Ref No": "", "Client": "Unknown" }
]
}'Add ?dryRun=true to see what each item would become without writing anything — useful while you wire up a new source.
{
"success": true,
"dryRun": true,
"data": [
{ "index": 0, "ok": true, "value": { "reference": "LC-0142", "customerName": "Ama Mensah", "consolidationRef": "MSKU1234567", "eta": "2026-10-20T00:00:00.000Z" } },
{ "index": 1, "ok": false, "errors": ["Reference is empty."] }
]
}Use case: you receive container events from a shipping line or a tracking aggregator. Forward them in the DCSA Track & Trace format; Lyverto matches each to your container by equipmentReference (or a document reference) and proposes a customer-friendly update.
curl -X POST https://lyverto.com/api/v1/feeds/dcsa \
-H "Authorization: Bearer lyv_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '[{
"eventID": "7c1e…",
"eventType": "EQUIPMENT",
"eventClassifierCode": "ACT",
"equipmentEventTypeCode": "DISC",
"eventDateTime": "2026-10-19T22:10:00Z",
"equipmentReference": "MSKU1234567",
"eventLocation": { "locationName": "Tema", "UNLocationCode": "GHTEM" }
}]'That event becomes the proposal “Discharged from vessel at Tema (GHTEM)”. Estimated vessel arrivals (EST + ARRI) become ETA-change proposals. To skip review for routine events, turn on the “Approve routine carrier events automatically” automation in Lyverto.
Use case: your system should know when something happens in Lyverto — a customer was told, an update was approved, a customer sent a WhatsApp message.
Add an endpoint in Settings → Integrations → Webhooks (or subscribe with POST /hooks). Lyverto POSTs batches of events, in order, and resumes where it stopped if your endpoint is down. Delivery is at least once: respond 2xx quickly and de-duplicate on seq.
{
"id": "<delivery-uuid>",
"organizationId": "<uuid>",
"sentAt": "2026-09-29T08:10:05.000Z",
"events": [
{
"seq": 1042,
"hash": "<sha256>",
"action": "tracking.event.appended",
"subjectType": "tracking_record",
"subjectId": "<uuid>",
"summary": "Added checkpoint \"Customs hold\" on consignment LC-0142",
"payload": { "reference": "LC-0142", "label": "Customs hold", "reason": "Physical inspection requested", "nextStep": "Clearance in 3–5 days", "…": "…" },
"actorKind": "human",
"source": "api",
"occurredAt": "2026-09-29T08:10:00.000Z"
}
]
}Verify every delivery. The Lyverto-Signature header is t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>"> using your endpoint’s signing secret. Reject signatures older than five minutes.
import crypto from "node:crypto";
export function verifyLyverto(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1 ?? "", "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}No public endpoint? Poll GET /events with the cursor from your last call. Store cursor.after and pass it back as after.
curl "https://lyverto.com/api/v1/events?after=1041&limit=100" \
-H "Authorization: Bearer lyv_YOUR_API_KEY"action | Happens when |
|---|---|
tracking.record.created / .updated / .published | A consignment was created, edited, or made visible to its customer |
tracking.event.appended | A timeline update was recorded (by a person, an agent, a feed or your API) |
tracking.broadcast.withdrawn | A container update was taken back |
tracking.proposal.created / .approved / .rejected | An update entered Review, or was decided |
tracking.share.created / .revoked | A consignment was forwarded to a partner, or access withdrawn |
tracking.customer.notified | An automation told a customer |
channel.message.received | A customer sent a WhatsApp message |
tracking.rule.*, integration.* | Automations, keys, webhooks, mappings or WhatsApp settings changed |
Use case: keep your own site and portal, and use Lyverto as the backend for shipment status. Each consignment carries trackingUrl — the customer’s live tracking page — once published, so you can link to it, or read the status and timeline and show them in your own design.
curl https://lyverto.com/api/v1/consignments/LC-0142 \
-H "Authorization: Bearer lyv_YOUR_API_KEY"Use case: connect Lyverto to Google Sheets, a CRM, email or accounting without writing code.
webhooks:manage scope for instant triggers.https://lyverto.com/api/v1/openapi.json, or use an HTTP step with your key.POST /hooks and remove it with DELETE /hooks/{id}.curl -X POST https://lyverto.com/api/v1/hooks \
-H "Authorization: Bearer lyv_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://hooks.example.com/catch/123", "events": ["tracking.event.appended"] }'/meany keyThe business and scopes this key acts for. Use it as a connection test.
curl https://lyverto.com/api/v1/me -H "Authorization: Bearer lyv_YOUR_API_KEY"/usageany keyYour plan, its limits, and today’s usage.
curl https://lyverto.com/api/v1/usage -H "Authorization: Bearer lyv_YOUR_API_KEY"/consignmentsscope: tracking:readYour consignments, most recently changed first. For polling, pass the time of your last poll as updatedSince.
status stringdraft, published, in_transit, delivered, on_hold or cancelledconsolidationRef stringupdatedSince ISO date-timelimit integercurl "https://lyverto.com/api/v1/consignments?status=in_transit&limit=20" \
-H "Authorization: Bearer lyv_YOUR_API_KEY"/consignmentsscope: tracking:writeCreate a consignment, or update the one with this reference. Only the fields you send are changed.
reference stringrequireddescription stringcustomerName stringcustomerEmail stringcustomerPhone string+234 803 123 4567consolidationRef stringorigin stringdestination stringeta ISO date-timestatus stringpublish booleancurl -X POST https://lyverto.com/api/v1/consignments \
-H "Authorization: Bearer lyv_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "reference": "LC-0142", "customerName": "Ama Mensah", "publish": true }'/consignments/{reference}scope: tracking:readOne consignment, with its customer-visible timeline in events.
curl https://lyverto.com/api/v1/consignments/LC-0142 -H "Authorization: Bearer lyv_YOUR_API_KEY"404 with reason: not_found when you have no consignment with that reference./consignments/{reference}scope: tracking:writeUpdate the fields you send. Same fields as POST /consignments, without reference and publish.
curl -X PATCH https://lyverto.com/api/v1/consignments/LC-0142 \
-H "Authorization: Bearer lyv_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "status": "in_transit", "eta": "2026-10-24T00:00:00Z" }'400 with reason: validation_failed when moving a published consignment back to draft./consignments/{reference}/eventsscope: tracking:readJust the customer-visible timeline, oldest first.
curl https://lyverto.com/api/v1/consignments/LC-0142/events -H "Authorization: Bearer lyv_YOUR_API_KEY"/eventsscope: tracking:writeSubmit timeline updates. Each goes to Review (propose, the default) or straight to the timeline (append).
mode "propose" | "append"proposeevents[].target objectrequiredreference, consolidationRef or trackingIdevents[].label stringrequiredevents[].externalEventId stringevents[].code stringevents[].reason stringevents[].nextStep stringevents[].occurredAt ISO date-timeevents[].location stringevents[].latitude / longitude numberevents[].eta ISO date-timeevents[].standardCode stringdcsa:DISCcurl -X POST https://lyverto.com/api/v1/events \
-H "Authorization: Bearer lyv_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "events": [{ "target": { "reference": "LC-0142" }, "externalEventId": "tms-1", "label": "Picked up", "code": "picked_up" }] }'400 with reason: batch_too_large over your plan’s batch size./eventsscope: events:readEverything that happened in your business, in order, by cursor.
after integercursor.after from your previous call (0 to start)limit integercurl "https://lyverto.com/api/v1/events?after=0" -H "Authorization: Bearer lyv_YOUR_API_KEY"/ingest/{mapping}scope: tracking:writeSend any JSON — an object, an array, or an object holding the array — through a saved mapping.
dryRun booleantrue to see the mapped items without writingcurl -X POST "https://lyverto.com/api/v1/ingest/warehouse-sheet?dryRun=true" \
-H "Authorization: Bearer lyv_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "rows": [{ "Ref No": "LC-0142", "Client": "Ama Mensah" }] }'404 when no mapping has that name; 409 with reason: mapping_paused when it is paused./mappingsscope: tracking:readYour saved mappings.
curl https://lyverto.com/api/v1/mappings -H "Authorization: Bearer lyv_YOUR_API_KEY"/feeds/dcsascope: tracking:writeDCSA Track & Trace events (a bare array, or { "events": [...] }). Each is matched to your container or consignment and proposed.
curl -X POST https://lyverto.com/api/v1/feeds/dcsa \
-H "Authorization: Bearer lyv_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '[{ "eventID": "…", "eventType": "TRANSPORT", "eventClassifierCode": "ACT", "transportEventTypeCode": "DEPA", "eventDateTime": "2026-09-29T06:00:00Z", "equipmentReference": "MSKU1234567" }]'skipped with the reason./hooksscope: webhooks:manageSubscribe a URL to events. Omit events for everything. The signing secret is returned once.
url stringrequiredhttps:// URLevents string[]["tracking.event.appended"]curl -X POST https://lyverto.com/api/v1/hooks \
-H "Authorization: Bearer lyv_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://your-system.example.com/lyverto" }'/hooks/{id}scope: webhooks:manageUnsubscribe. A key can only remove hooks it created.
curl -X DELETE https://lyverto.com/api/v1/hooks/<id> -H "Authorization: Bearer lyv_YOUR_API_KEY"| HTTP | reason | What to do |
|---|---|---|
| 400 | validation_failed | Fix the field named in details or the message |
| 400 | batch_too_large | Split the batch to your plan’s size |
| 401 | invalid_api_key | Check the key; it may be revoked or expired |
| 403 | scope_required | Create a key with the scope named in the message |
| 404 | not_found | Nothing of yours matches |
| 409 | conflict / mapping_paused | The current state prevents it — read the message |
| 429 | rate_limited | Wait Retry-After seconds, then retry |
| 429 | quota_exceeded | Daily quota used; resets 00:00 UTC, or upgrade |
| 500 | — | Retry with backoff; quote the Request-Id if it persists |
Fields the API does not recognise are ignored, not refused, so an extra field never breaks a request. The response lists each one in warnings — for example { "field": "custmerEmail", "message": "Unknown field; ignored." } — so a typo shows up in your logs instead of silently doing nothing.