Pharmacies API

Pharmacies and medical stores: catalogue, orders, branches, webhooks, bots and autonomous delivery units.

Root types are PharmacyQuery and PharmacyMutation.

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.

Catalogue

OperationNotes
Cursor-paginated; nodes are Product
Fetch one
Currency is taken from your vendor configuration, not the input
Modify
Soft delete; returns true

Prices are Decimal — a string on the wire. Parse into a decimal type, never a float.

Location menus

A menu groups your products for one of your locations, so each branch can carry its own offering.

OperationNotes
Every menu, or one location's. Not paginated
Product IDs, in menu order
vendorLocationId must be one of your locations
Name, sort position and visibility
Delete a menu
Idempotent. The product must be yours
The product itself is untouched

A menu's location is fixed when it is created: the update input has no location field.

vendorMenuProducts returns IDs only; fetch the details with product(productId:). Adding a product that is already on the menu leaves it where it is. inOrder is a menu's sort position among your menus, lowest first.

A menu's name is localised. On update, the translations you send are merged into the existing ones; set replace: true to replace them all. Reading name { value } resolves to the request's Accept-Language, falling back to English and then to any translation that exists. Pass value(locale:) for one language, or select values for every translation.

There is no operation that reads or sets a product's price per location.

Stock levels

vendorInventory takes no arguments and lists only the products that need attention. outOfStockItems holds products with nothing left, and products marked unavailable — a product with stock on the shelf that a customer cannot buy counts as out of stock. lowStockItems holds products at or below their minimumThreshold. A product with healthy stock appears in neither.

An item's id is the product ID, not the ID of its stock record. Stock is held per location and an item does not say which location it comes from, so a product stocked at several locations can appear once for each.

alerts is a summary built on each call — at most one high alert for out-of-stock products and one medium alert for low stock — not a stored history.

Orders

order(orderId:) fetches one.

Lifecycle

OrderStatus:

You drive four transitions:

MutationMoves
PENDING → ACCEPTED
ACCEPTED → PREPARING
PREPARING → READY
PENDING or ACCEPTED → CANCELLED

Transitions are ordered — an order cannot be marked ready before preparing. Cancellation is only possible while PENDING or ACCEPTED. ENROUTE, DELIVERED and COMPLETED reflect fulfilment and are not settable by you.

Payment status

PaymentStatus is tracked separately: PENDING, REQUEST, REVIEW, FAILED, CANCELLED, SUCCESSFUL, CAPTURE_FAILED, REFUNDED.

An order can be ACCEPTED while payment is still PENDING. Do not infer one from the other.

Coupons

Discount codes that run on your own storefront.

OperationNotes
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.

OperationNotes
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.

periodWindow
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:

  • dailySales has 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.
  • growthRate is the percentage change against the preceding window of the same length. It reads 0 when that window sold nothing.
  • peakHours holds 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 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.

GroupValues
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

HeaderContents
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.

OperationNotes
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 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.

EnumValues
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.

Built with