Parcels API
Courier and parcel delivery: parcel orders, package pricing, branches, webhooks, bots and autonomous delivery units.
Root types are ParcelQuery and ParcelMutation.
Start with Authentication.
Profile and settings
profile returns the authenticated vendor's profile; settings returns the
settings blob. Both are read-only through this API.
Parcel orders
Cursor-paginated. order(orderId:) fetches one.
These are named orders / order rather than orders / order,
but return the same OrderConnection / Order types as other
verticals — so the page of results is nodes, as everywhere else.
Filterable by OrderStatus: SCHEDULED, PENDING, ACCEPTED, PREPARING,
READY, ENROUTE, DELIVERED, COMPLETED, FAILED, CANCELLED.
PaymentStatus is tracked separately.
Parcels exposes no order-transition mutations — there is no acceptOrder
equivalent. Parcel orders are progressed by the courier and the platform; this
API is read-only over them. Use webhooks to observe progress.
Package pricing
One pricing rule per package type.
Cursor-paginated.
| Mutation | Effect |
|---|---|
Create or replace the rule for a package type | |
Change the price of an existing rule | |
Remove; returns true |
price is Decimal — a string on the wire. Parse into a decimal type,
never a float.
Coupons
Discount codes that run on your own storefront.
| Operation | Notes |
|---|---|
The coupons you own. Not paginated | |
The code must be unique across the platform | |
Change a coupon you own | |
Delete a coupon you own |
vendorCoupons lists only the coupons your vendor owns. Platform coupons your
storefront happens to qualify for are not included, and cannot be changed
here.
A code is unique across the whole platform, not just your storefront, so
createVendorCoupon rejects a code another vendor already uses.
discount is a percentage when percentage is true and a fixed amount
otherwise, and it can never be negative. maxCouponAmount caps a percentage
discount and is ignored for a fixed one. minOrderAmount is in your currency.
All three are Decimal — a string on the wire.
productIds and vendorLocationIds narrow where the coupon applies; an empty
list means all of your products, or all of your locations. On
updateVendorCoupon either list replaces the existing restriction outright,
and an empty list clears it. times caps redemptions across all customers.
expiresOn is UTC; omit it for a coupon that never expires.
The three coupon mutations need vendors:promotions:write, which a coarse
vendors grant does not include. Request it by name — see
Authentication.
Reviews and performance
Read-only figures on how your business is doing.
| Operation | Notes |
|---|---|
Sales, top products and trends for a window | |
Acceptance, delivery success, timing and rating | |
Most recent reviews first |
The two analytics queries need vendors:analytics:read; vendorReviews needs
vendors:reviews:read. See Authentication.
Sales analytics
period and date are plain Strings, not enums, so a typo is not caught
by query validation.
period | Window |
|---|---|
today | From midnight in your timezone until now |
week | The last 7 days, ending now |
month | The last 30 days, ending now. The default |
year | The last 365 days, ending now |
Any other value | The single day named by date, in your timezone |
For a single day, pass date — YYYY-MM-DD or an RFC 3339 timestamp —
together with a period that is none of the four names. An unknown period
without a usable date is rejected rather than widened to all time. Passing
date on its own does not work: period then defaults to month, and
date is ignored.
Your timezone is that of your location with the lowest ID, or UTC if you have no locations.
In topProducts, up to ten products are ranked by revenue. In trends:
dailySaleshas one entry per day that saw a counted order, in date order. Days without one are skipped, and entries carry no date, so the series is not continuous.growthRateis the percentage change against the preceding window of the same length. It reads0when that window sold nothing.peakHoursholds up to three hours of the day (0–23, your timezone), busiest first.
Sales figures count only orders whose status is DELIVERED, by when they were
placed. An order in any other status — COMPLETED included — is left out. A
window is read up to 10,000 orders, and past that the figures understate.
Verified against the backend source as of 2026-09-17; tracked in
backend#909.
Performance metrics
vendorPerformanceMetrics takes no
arguments, and its order figures cover the last 30 days.
orderAcceptanceRate and deliverySuccessRate are percentages, 0–100.
avgPreparationTime is the average number of minutes from order creation to
delivery, across delivered orders. Despite the name, it is not preparation
time.
customerSatisfaction is your average star rating out of 5. It is averaged
over all of your reviews, not only the last 30 days.
Every figure reads 0 both when there is nothing to measure and when it could
not be worked out, so a 0 is not proof of a real zero.
Reviews
vendorReviews returns your most recent reviews
first. limit defaults to 20 and is capped at 100; the response's limit
echoes the value applied. There is no pagination, so reviews past the cap
cannot be reached. ratingFilter keeps a single star rating, 1 to 5, and any
other value is rejected.
A review's date is YYYY-MM-DD HH:MM:SS in UTC, not RFC 3339. orderId is
0 when the review is not tied to an order, and comment is empty when the
customer left a rating without words.
Locations
Your branches.
Cursor-paginated.
location(locationId:) fetches one. createLocation, updateLocation and
deleteLocation manage them; delete returns true.
To stop trading at a branch temporarily, deactivate via updateLocation rather
than deleting — deletion loses the association with historical records.
Webhooks
The alternative to polling. There are no GraphQL subscriptions, and polling for new parcel orders every few seconds will exhaust your rate limit.
webhooks(first:, after:) lists them.
webhook(webhookId:) fetches one. updateWebhook and deleteWebhook manage
them; set isActive: false to pause without losing configuration.
events is the WebhookEvent enum, so a typo is now a query validation error
rather than a subscription that silently never fires.
| Group | Values |
|---|---|
Orders | ORDER_CREATED, ORDER_UPDATED, ORDER_ASSIGNED, ORDER_CANCELLED, ORDER_COMPLETED |
Bookings | BOOKING_CREATED, BOOKING_CONFIRMED, BOOKING_CANCELLED, BOOKING_COMPLETED |
Payments | PAYMENT_RECEIVED, PAYMENT_REFUNDED |
Catalogue and customers | PRODUCT_LOW_STOCK, REVIEW_CREATED, CUSTOMER_MESSAGE |
Autonomous units | UNIT_REGISTERED, UNIT_VERIFIED, UNIT_ASSIGNED, UNIT_STATUS_CHANGED, UNIT_DELIVERY_COMPLETED, UNIT_MAINTENANCE_REQUIRED, UNIT_TELEMETRY_ALERT |
The enum lists values this vertical will never emit. Subscribing to one is accepted and simply never fires.
The enum spelling and the wire spelling differ. On the wire an event is
dot-separated — ORDER_CREATED is delivered as order.created — and that is
what the X-Kesk-Event header carries, what the payload's eventType key
holds, and what is stored against the subscription. The underscored form exists
only because a GraphQL enum value cannot contain a dot. Match on the header
value you actually receive rather than deriving it from the enum name.
Verifying deliveries
| Header | Contents |
|---|---|
X-Kesk-Signature | HMAC-SHA256(secret, raw_request_body), lowercase hex, no prefix |
X-Kesk-Event | The event's wire name, e.g. order.created |
The body is the event's fields plus an eventType key naming the event.
Sign the raw bytes. Parsing the JSON and re-stringifying it changes key order or whitespace, and every valid delivery will fail.
Deliveries time out after 10 seconds and are attempted at most 3 times, retrying 30 seconds after the first failure and 5 minutes after the second. A non-2xx response, a timeout and a transport error all count as failures. Redirects are not followed — point the webhook at its final URL.
Return 2xx as soon as you have durably accepted the event, then process
asynchronously. Be idempotent — retries mean you will see events more than once.
When a delivery exhausts its three attempts the webhook is disabled —
isActive flips to false and it stops receiving events entirely until you
re-enable it. An endpoint that is down long enough does not just miss those
deliveries; it stops being a webhook. Monitor isActive.
Every attempt is recorded with its request body, response status and failure reason. Response bodies are not retained.
Getting the signing secret
createWebhook does not return a signing secret. Call
regenerateWebhookSecret(webhookId:) once after registering to obtain it —
that is the only time it is shown. Until you do, you have a webhook whose
deliveries you cannot verify.
The same mutation rotates the secret later. The value is stored hashed and cannot be retrieved again.
Bots
Automations registered against your account.
BotType: WEBHOOK, SCRIPTED, AI_POWERED.
BotStatus: ACTIVE, INACTIVE, SUSPENDED, PENDING_REVIEW.
| Operation | Notes |
|---|---|
Permissions, version and tags are not editable afterwards | |
Cannot change permissions, version or tags | |
Permanent | |
Queue a test delivery to a WEBHOOK bot | |
Rotate, returns the new secret | |
Totals, webhook health, daily series. days defaults to 7, max 90 |
Autonomous units
Drones and ground robots for parcel delivery.
Cursor-paginated. unit(id:) fetches one. Besides
status and verificationStatus, units filters on isActive,
isDispatchable, hasCredential, an exact serialNo, and maxBatteryPct
for units at or below a charge level.
| Enum | Values |
|---|---|
AutonomousUnitKind | DRONE, GROUND_ROBOT, SIDEWALK_ROBOT |
AutonomousUnitStatus | OFFLINE, IDLE, ASSIGNED, ENROUTE, RETURNING, CHARGING, MAINTENANCE, DISABLED |
AutonomousVerificationStatus | PENDING, IN_REVIEW, VERIFIED, REJECTED, SUSPENDED |
registerUnit registers a unit, owned by the vendor your token acts
for — the input has no owner field. Nor does it take an operating area: that
is set by an administrator, and updateUnit cannot
change it either. Verification status is not settable; units start unverified
and are moved through review by the platform.
setUnitStatus reports operational state, restricted to states an owner may
legitimately report. ASSIGNED, ENROUTE and RETURNING follow from
dispatch and DISABLED is an administrative act, so none of them is settable
by you.
isDispatchable says whether a unit can be sent on a job right now: in
service, verified, and in a state that accepts work. Use it rather than
deriving your own check from battery and verification fields.
Machine credentials
The plaintext secret is returned exactly once and cannot be retrieved
afterwards. Re-provisioning an already-credentialed unit is refused rather
than silently rotating, which would strand the unit in the field holding a
secret nobody else has. To rotate, revoke first with
revokeUnitCredential(id:), then provision.
The credential's scopes are the same fixed set on every unit:
units:telemetry:write, units:jobs:read, units:jobs:write and
units:status:write. Provisioning grants all four — there is nothing to
narrow and nothing to choose. A unit cannot read your catalogue or touch orders.
These are absent from the issuer's scopes_supported on purpose: that document
advertises what a developer application may request, and no application can
ask for a units:* scope. The hardware need not send them either — a unit
that leaves scope out of its token request is granted all four.
Reference
Generated reference — every type, field and argument, built from the live schema.