The Kelvara API
One REST API for shipments, routes, drivers, lockers and tracking events. JSON over HTTPS, predictable resources, idempotent writes and signed webhooks. Sandbox credentials are issued with every trial account.
# 1. Create a shipment in the sandbox
curl https://api.sandbox.dpd-lt.com/v1/shipments \
-H "Authorization: Bearer $KELVARA_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d @shipment.json
# 2. Watch it move
curl https://api.sandbox.dpd-lt.com/v1/shipments/shp_9F41CX22/events \
-H "Authorization: Bearer $KELVARA_API_KEY"
How the API behaves
There are no surprises here on purpose. If you have integrated a modern payments or messaging API, this will feel familiar.
Base URLs
Production is api.dpd-lt.com and sandbox is
api.sandbox.dpd-lt.com. Sandbox is a full copy of the
platform with simulated vehicles that move in accelerated time, so you can watch a route
complete in minutes.
Authentication
A bearer token in the Authorization header. Keys are scoped
per environment and per permission set — a key that can read tracking events cannot create
shipments unless you grant it. Rotate keys from the console at any time; the previous key
stays valid for a 24-hour overlap.
Versioning
The version is in the path (/v1/). Additive changes — new
fields, new event types, new optional parameters — ship without a version bump, so parse
responses tolerantly. Breaking changes get a new version and a minimum 12-month overlap.
Idempotency
Every POST accepts an Idempotency-Key
header. Replaying a request with the same key returns the original response instead of
creating a duplicate shipment. Keys are retained for 48 hours — long enough to survive a
retry storm during peak.
Pagination
Cursor-based. List endpoints return data plus a
next_cursor; pass it back as ?cursor=.
Offset pagination is not supported because operational data moves underneath you while you
page through it.
Rate limits
60 requests per second on Starter, 250 on Growth, negotiated on Enterprise. Every response
carries X-RateLimit-Remaining and
X-RateLimit-Reset. Exceeding the limit returns
429 with a Retry-After header —
honour it rather than spinning.
Core resources
Five resource families cover most integrations. Everything below is described in full in the
OpenAPI document, which you can pull from
api.dpd-lt.com/v1/openapi.json.
Shipments
A shipment is one consignment to one destination. It holds the recipient, the service level, one or more parcels, and the delivery preferences that constrain how it can be fulfilled.
| Method | Path | Description |
|---|---|---|
| POST | /v1/shipments | Create a shipment. Returns a tracking code and the resolved, normalised destination address. |
| GET | /v1/shipments/{id} | Retrieve a single shipment with its current status and ETA. |
| GET | /v1/shipments | List shipments, filterable by status, depot, date range or reference. |
| PATCH | /v1/shipments/{id} | Update delivery preferences or recipient contact details before dispatch. |
| DELETE | /v1/shipments/{id} | Cancel a shipment. Rejected once the parcel is loaded onto a vehicle. |
| GET | /v1/shipments/{id}/events | The full ordered event history for a shipment. |
| GET | /v1/shipments/{id}/proof | Proof-of-delivery record, including signed URLs for signature and photo media. |
Routes & planning
Planning is asynchronous. You submit a plan request describing the depot, the vehicles available and the shipments to include; the optimiser returns a plan you can accept, adjust or discard.
| Method | Path | Description |
|---|---|---|
| POST | /v1/plans | Submit a planning run. Returns a plan ID immediately; completion arrives by webhook. |
| GET | /v1/plans/{id} | Plan status, summary metrics and the resulting routes once solved. |
| POST | /v1/plans/{id}/commit | Accept a plan and release its routes to drivers. |
| GET | /v1/routes/{id} | A single route with its ordered stops, assigned vehicle and driver. |
| POST | /v1/routes/{id}/replan | Reoptimise the remaining stops on an in-progress route. |
Lockers
| Method | Path | Description |
|---|---|---|
| GET | /v1/lockers | List cabinets, filterable by proximity to a coordinate or postal code. |
| GET | /v1/lockers/{id}/availability | Free compartments by size class, refreshed continuously. |
| POST | /v1/lockers/{id}/reservations | Hold a compartment for a shipment ahead of dispatch. |
| DELETE | /v1/reservations/{id} | Release a held compartment back to the pool. |
Addresses
| Method | Path |
|---|---|
| POST | /v1/addresses/resolve |
| GET | /v1/addresses/autocomplete |
Resolve takes a free-text string and returns a canonical Baltic address with a confidence score, or a list of candidates when the input is genuinely ambiguous.
Fleet & drivers
| Method | Path |
|---|---|
| GET | /v1/vehicles |
| POST | /v1/vehicles |
| GET | /v1/drivers |
| GET | /v1/drivers/{id}/shifts |
Vehicles carry capability flags — cooling, tail lift, capacity by volume and weight — that the optimiser treats as hard constraints.
Creating a shipment
A minimal create call and the response you get back. Note that the address you send is echoed back normalised, with a confidence score you can act on before the parcel is picked.
curl -X POST https://api.dpd-lt.com/v1/shipments \
-H "Authorization: Bearer $KELVARA_API_KEY" \
-H "Idempotency-Key: 8f2c1e04-ord-55219" \
-H "Content-Type: application/json" \
-d '{
"reference": "ORD-55219",
"service_level": "next_day",
"depot_id": "dep_KAU01",
"destination": {
"name": "Rasa Jankauskienė",
"street": "Verslo al. 12-4",
"city": "Kaunas",
"postal_code": "LT-44210",
"country": "LT",
"phone": "+37060000000"
},
"parcels": [
{ "weight_g": 1450, "size_class": "M" }
],
"preferences": {
"locker_allowed": true,
"signature_required": false,
"leave_with_neighbour": false
}
}'
{
"id": "shp_9F41CX22",
"object": "shipment",
"reference": "ORD-55219",
"tracking_code": "KLV-8841207",
"tracking_url": "https://track.dpd-lt.com/KLV-8841207",
"status": "created",
"service_level": "next_day",
"destination": {
"street": "Verslo al.",
"house_number": "12",
"unit": "4",
"city": "Kaunas",
"postal_code": "LT-44210",
"country": "LT",
"resolution": {
"status": "exact",
"confidence": 0.98
}
},
"promised_window": {
"from": "2026-07-15T09:00:00Z",
"to": "2026-07-15T17:00:00Z"
},
"created_at": "2026-07-14T08:11:02Z"
}
Errors
Errors use conventional HTTP status codes and a consistent body. The
type field is stable and safe to branch on; the
message is for humans and may change.
{
"error": {
"type": "address_unresolvable",
"message": "Destination address could not be resolved with sufficient confidence.",
"param": "destination.street",
"request_id": "req_01HQ8M2K4Z7Y",
"candidates": [
{ "street": "Verslo al.", "city": "Kaunas", "confidence": 0.61 },
{ "street": "Verslo g.", "city": "Kaunas", "confidence": 0.44 }
]
}
}
Always log request_id. It is the fastest way for our support team
to find exactly what happened.
Events you can subscribe to
Register endpoint URLs in the console, choose the event types you care about, and Kelvara
will POST each one as JSON. Delivery is at-least-once with exponential backoff over 24 hours,
so make your handlers idempotent on the event id.
| Event type | When it fires |
|---|---|
| Shipment lifecycle | |
shipment.created | A shipment has been accepted and given a tracking code. |
shipment.assigned | The shipment has been placed on a committed route or reserved into a locker. |
shipment.out_for_delivery | The vehicle carrying the parcel has left the depot. |
shipment.eta_updated | The predicted arrival window has moved beyond the notification threshold. |
shipment.delivered | Delivery completed and proof captured. |
shipment.attempt_failed | An attempt did not succeed; the payload carries the reason code. |
shipment.returned | The parcel is on its way back to the depot or the sender. |
shipment.cancelled | Cancelled by the sender or by an operator before dispatch. |
| Planning | |
plan.completed | An optimisation run has finished and routes are ready to review. |
plan.failed | A run could not produce a feasible plan; the payload lists the binding constraints. |
route.committed | A route has been released to a driver. |
route.completed | Every stop on the route has reached a terminal state. |
| Lockers | |
locker.deposited | A courier has placed a parcel into a compartment. |
locker.collected | The recipient has opened the compartment and taken the parcel. |
locker.dwell_exceeded | A parcel has passed its configured collection grace period. |
| Exceptions | |
exception.opened | An exception has been raised against a shipment, route or cabinet. |
exception.resolved | An operator has closed the exception. |
coldchain.excursion | A monitored consignment has left its permitted temperature range. |
Verifying signatures
Each delivery carries a Kelvara-Signature header containing a
timestamp and an HMAC-SHA256 of timestamp + "." + raw_body,
computed with your endpoint's signing secret. Compare using a constant-time function and
reject anything with a timestamp older than five minutes.
Verify against the raw request body, before any JSON parsing or re-serialisation — reserialised JSON will not match.
import crypto from "node:crypto";
export function verify(rawBody, header, secret) {
const [ts, sig] = header.split(",").map(p => p.split("=")[1]);
// Reject replays outside a five-minute window.
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(sig)
);
}
2xx quickly and do your work asynchronously. Kelvara treats
anything slower than 10 seconds as a failure and will retry, which is how duplicate processing
starts.
SDKs and tooling
Official clients are generated from the same OpenAPI document that backs the API, then hand-finished for ergonomics. All are MIT licensed and handle auth, retries with backoff, idempotency keys and cursor pagination for you.
TypeScript
Node 18+ and edge runtimes. Fully typed responses and discriminated unions for event payloads.
npm i @kelvara/sdk
Python
3.9+, sync and async clients, type stubs included.
pip install kelvara
PHP
8.1+, PSR-18 compatible, common for storefront integrations.
composer require kelvara/sdk
Java
17+, published to Maven Central. Used mostly for WMS and ERP integrations.
com.kelvara:sdk
OpenAPI document
Generate a client for any language from /v1/openapi.json. Versioned alongside the API.
Webhook replay tool
Replay any past event from the console against a local tunnel while you build a handler.
Postman collection
Every endpoint with sandbox examples, kept in step with each release.
Want sandbox credentials?
Sandbox access comes with every trial, including simulated vehicles you can watch complete a route in accelerated time. Tell us what you are integrating and we will get you a key.