MedaKi Ingest API
The MedaKi Ingest API allows your pharmacy management system to sync product inventory directly with the MedaKi platform. Once integrated, your product catalog — including pricing, stock levels, and product details — will stay up to date in real time.
Getting Started
Environments
Each environment requires its own set of API credentials. To request your staging or production keys, contact the MedaKi team.
| Environment | Base URL |
|---|---|
| Staging | https://stg-ingest.medaki.ca |
| Production | https://ingest.medaki.ca |
Authentication
Every request must include two headers to identify and verify your system:
| Header | Description |
|---|---|
X-API-Key | Your public API key |
X-Hook-Signature | An HMAC SHA-256 signature of the request body, signed with your secret key |
The signature ensures that requests cannot be tampered with in transit. It is computed from the raw JSON body of each request — if the body is empty, sign an empty string.
async function sign(body, secretKey) {
const encoder = new TextEncoder();
const cryptoKey = await crypto.subtle.importKey(
'raw',
encoder.encode(secretKey),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(body));
return Array.from(new Uint8Array(signature))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
const body = JSON.stringify(payload);
const signature = await sign(body, secretKey);
How It Works
Product submissions are processed asynchronously. When you send a batch of products, the API accepts the request immediately and returns a request_id. You then poll the Operations endpoint to track progress until the batch completes.
1. POST /products → 202 Accepted { request_id: "abc123", chunks: 10 }
2. GET /operations/abc123/status → { status: "processing", progress: 60 }
3. GET /operations/abc123/status → { status: "completed", processed_products: 5000 }
Large batches are split into chunks of 500 products processed in parallel. Typical throughput is 2–5 seconds per chunk, with up to 3 chunks running concurrently. If a batch fails partway through, you can re-queue it using the retry endpoint without resubmitting the entire batch.
Endpoints
Health
GET /health
Verify that the API is reachable and operational. Does not require authentication.
Response
{
"status": 200,
"success": true,
"message": "healthy",
"elapsed_ms": 45
}
Products
GET /products
Retrieve your pharmacy's current product catalog from the platform, with support for pagination and GTIN filtering.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
pharmacy_id | string | Yes | Your pharmacy identifier |
gtin | string | No | Retrieve a specific product by GTIN |
skip | number | No | Number of records to skip for pagination (default: 0) |
Response
{
"status": 200,
"success": true,
"data": [
{
"id": 1,
"gtin": "1234567890123",
"name": "Product Name",
"description": "Product description",
"price": 19.99,
"stock": 100,
"provider": "positec",
"from_pharmacy_id": "pharmacy123"
}
],
"pagination": {
"count": 1000,
"has_more": true,
"cursor": "next_page_token"
},
"elapsed_ms": 234
}
POST /products
Submit a batch of products for creation or update. The API responds immediately with a requestId you can use to monitor progress. See How It Works for the full workflow.
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
pharmacy_id | string | Yes | Your pharmacy identifier |
medaki_pharmacy_id | string | Yes | The MedaKi-assigned ID for your pharmacy |
action | string | Yes | Operation type — "create" or "update" |
products | array | Yes | List of products to sync — see Product Fields |
Request Body
{
"pharmacy_id": "pharmacy123",
"medaki_pharmacy_id": "medaki456",
"action": "create",
"products": [
{
"gtin": "1234567890123",
"name": "Product Name",
"description": "Product description",
"brand": "Brand Name",
"old_name": "Previous Product Name",
"old_gtin": "0987654321098",
"category": "Category",
"image_url": "https://example.com/image.jpg",
"stock": 100,
"price": 19.99,
"sale_price": 15.99,
"distributor": "Distributor Name"
}
]
}
Response — 202 Accepted
{
"status": 202,
"success": true,
"data": {
"message": "Product ingest queued for processing",
"request_id": "12345678",
"total_products": 1500,
"chunks": 3,
"status_url": "/operations/12345678/status"
},
"elapsed_ms": 45,
"request_id": "12345678"
}
Operations
GET /operations/{requestId}/status
Check the progress of a product submission. Poll this endpoint after calling POST /products until the status is completed or failed.
Response
{
"status": 200,
"success": true,
"data": {
"request_id": "12345678",
"status": "processing",
"total_chunks": 3,
"completed_chunks": 2,
"total_products": 1500,
"processed_products": 1000,
"failed_products": 0,
"progress": 67,
"start_time": 1647329400000,
"last_update": 1647329430000
}
}
Status Values
| Status | Description |
|---|---|
queued | Accepted and waiting to begin |
processing | Actively processing products |
completed | All products synced successfully |
failed | Processing failed — use the retry endpoint to re-queue |
GET /operations/machine
Retrieve a history of recent submissions made by your integration, useful for auditing and troubleshooting.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | number | No | Number of records to return (1–100, default: 50) |
Response
{
"status": 200,
"success": true,
"data": {
"machine_id": "b0586d19-4c05-4543-9bc8-ac9232a3f566",
"operations": [
{
"request_id": "12345678",
"status": "completed",
"total_chunks": 3,
"completed_chunks": 3,
"total_products": 1500,
"processed_products": 1500,
"failed_products": 0,
"start_time": 1647329400000,
"last_update": 1647329430000,
"end_time": 1647329450000,
"error": null,
"progress": 100
}
],
"total": 1,
"limit": 50
},
"elapsed_ms": 45,
"request_id": "87654321"
}
POST /operations/{requestId}/retry
Re-queue a failed operation. Only the failed portions of the original batch are retried — successfully processed products are not resubmitted.
Response
{
"status": 202,
"success": true,
"data": {
"message": "Operation retry initiated successfully",
"request_id": "12345678",
"failed_chunks_retried": 2,
"retry_attempt": 1
},
"elapsed_ms": 45,
"request_id": "12345678"
}
Reference
Product Fields
The products array in POST /products accepts the following fields.
Required — Create
| Field | Type | Notes |
|---|---|---|
gtin | string | Global Trade Item Number — must be unique per pharmacy |
name | string | Product display name |
brand | string | Brand name |
stock | number | Current quantity on hand |
price | number | Regular price — must be greater than 0 |
sale_price | number | Sale price — must be greater than 0 |
Required — Update
| Field | Type | Notes |
|---|---|---|
gtin | string | Identifies the existing product to update |
Optional
| Field | Type | Notes |
|---|---|---|
old_gtin | string | If the product's GTIN has changed, provide the previous value here to rename the existing record rather than create a duplicate |
old_name | string | Previous product name |
description | string | Product description |
category | string | Product category |
image_url | string | URL to the product image |
distributor | string | Distributor name |
Errors
All error responses follow a consistent structure:
{
"status": 4xx | 5xx,
"success": false,
"error": "Description of the error",
"request_id": "12345678"
}
| Status | Meaning | Example |
|---|---|---|
400 | Invalid request or missing required fields | "Validation failed: gtin is required" |
400 | Duplicate GTINs within the same payload | "Duplicate GTINs found in payload: 123, 456" |
401 | Missing or invalid API credentials | "Invalid API key" |
404 | The requested operation does not exist | "Operation not found" |
408 | Request timed out — try again shortly | "Database operation timed out" |
413 | Request body exceeds the 10 MB limit | "Request too large. Maximum size allowed: 10485760 bytes (10 MB)" |
500 | Unexpected server error | "Internal server error" |
503 | Platform temporarily unavailable | "Database is currently unavailable" |
Limits
| Constraint | Value |
|---|---|
| Max request size | 10 MB |
| Max products per request | 25,000 |
Code Examples
Node.js — Submit and Poll
Covers authentication, product submission, and progress polling.
const crypto = require('crypto');
const BASE_URL = process.env.INGEST_BASE_URL; // https://stg-ingest.medaki.ca or https://ingest.medaki.ca
const API_KEY = process.env.API_KEY;
const SECRET_KEY = process.env.SECRET_KEY;
async function sign(body) {
const encoder = new TextEncoder();
const cryptoKey = await crypto.subtle.importKey(
'raw',
encoder.encode(SECRET_KEY),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const sig = await crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(body));
return Array.from(new Uint8Array(sig))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
async function submitProducts(products) {
const payload = {
pharmacy_id: 'pharmacy123',
medaki_pharmacy_id: 'medaki456',
action: 'create',
products,
};
const body = JSON.stringify(payload);
const signature = await sign(body);
const res = await fetch(`${BASE_URL}/products`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY,
'X-Hook-Signature': signature,
},
body,
});
return res.json();
}
async function pollUntilDone(requestId, intervalMs = 3000) {
while (true) {
const res = await fetch(`${BASE_URL}/operations/${requestId}/status`, {
headers: { 'X-API-Key': API_KEY, 'X-Hook-Signature': await sign('') },
});
const { data } = await res.json();
console.log(`Progress: ${data.progress}% — ${data.status}`);
if (data.status === 'completed' || data.status === 'failed') return data;
await new Promise((r) => setTimeout(r, intervalMs));
}
}
const { data } = await submitProducts([/* your products */]);
const result = await pollUntilDone(data.requestId);
console.log(result);
cURL
# Health check
curl /health
# Get products
curl "/products?pharmacy_id=pharmacy123&skip=0" \
-H "X-API-Key: your_api_key" \
-H "X-Hook-Signature: your_signature"
# Submit products
curl -X POST /products \
-H "Content-Type: application/json" \
-H "X-API-Key: your_api_key" \
-H "X-Hook-Signature: your_signature" \
-d '{
"pharmacy_id": "pharmacy123",
"medaki_pharmacy_id": "medaki456",
"action": "create",
"products": [
{
"gtin": "1234567890123",
"name": "Product Name",
"brand": "Brand Name",
"stock": 100,
"price": 29.99,
"sale_price": 24.99
}
]
}'
# Check operation status
curl "/operations/12345678/status" \
-H "X-API-Key: your_api_key" \
-H "X-Hook-Signature: your_signature"
# List recent operations
curl "/operations/machine?limit=10" \
-H "X-API-Key: your_api_key" \
-H "X-Hook-Signature: your_signature"
# Retry a failed operation
curl -X POST "/operations/12345678/retry" \
-H "X-API-Key: your_api_key" \
-H "X-Hook-Signature: your_signature"
Changelog
2026-05-14
- BREAKING —
GET /healthresponse no longer includes a nesteddataobject. Database status is now reflected in the top-levelmessagefield ("message": "healthy"). Update any clients readingdata.databaseordata.timestamp.
2026-05-05
- BREAKING — Signature header renamed from
X-SignaturetoX-Hook-Signature. Update all client requests accordingly. - FIX —
GET /healthno longer requires authentication and is now publicly accessible. - FIX —
POST /operations/{requestId}/retrywas previously not handled as a POST — requests to this endpoint now work correctly. - FIX —
priceandsale_pricemust be positive numbers (> 0). Zero and negative values are rejected with 400. - FIX — GTIN changes (
old_gtin≠gtin) now correctly rename the existing product record instead of creating a duplicate.
