MedaKi

Contents

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.

EnvironmentBase URL
Staginghttps://stg-ingest.medaki.ca
Productionhttps://ingest.medaki.ca

Authentication

Every request must include two headers to identify and verify your system:

HeaderDescription
X-API-KeyYour public API key
X-Hook-SignatureAn 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

ParameterTypeRequiredDescription
pharmacy_idstringYesYour pharmacy identifier
gtinstringNoRetrieve a specific product by GTIN
skipnumberNoNumber 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

FieldTypeRequiredDescription
pharmacy_idstringYesYour pharmacy identifier
medaki_pharmacy_idstringYesThe MedaKi-assigned ID for your pharmacy
actionstringYesOperation type — "create" or "update"
productsarrayYesList 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"
    }
  ]
}

Response202 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

StatusDescription
queuedAccepted and waiting to begin
processingActively processing products
completedAll products synced successfully
failedProcessing 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

ParameterTypeRequiredDescription
limitnumberNoNumber 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

FieldTypeNotes
gtinstringGlobal Trade Item Number — must be unique per pharmacy
namestringProduct display name
brandstringBrand name
stocknumberCurrent quantity on hand
pricenumberRegular price — must be greater than 0
sale_pricenumberSale price — must be greater than 0

Required — Update

FieldTypeNotes
gtinstringIdentifies the existing product to update

Optional

FieldTypeNotes
old_gtinstringIf the product's GTIN has changed, provide the previous value here to rename the existing record rather than create a duplicate
old_namestringPrevious product name
descriptionstringProduct description
categorystringProduct category
image_urlstringURL to the product image
distributorstringDistributor name

Errors

All error responses follow a consistent structure:

{
  "status": 4xx | 5xx,
  "success": false,
  "error": "Description of the error",
  "request_id": "12345678"
}
StatusMeaningExample
400Invalid request or missing required fields"Validation failed: gtin is required"
400Duplicate GTINs within the same payload"Duplicate GTINs found in payload: 123, 456"
401Missing or invalid API credentials"Invalid API key"
404The requested operation does not exist"Operation not found"
408Request timed out — try again shortly"Database operation timed out"
413Request body exceeds the 10 MB limit"Request too large. Maximum size allowed: 10485760 bytes (10 MB)"
500Unexpected server error"Internal server error"
503Platform temporarily unavailable"Database is currently unavailable"

Limits

ConstraintValue
Max request size10 MB
Max products per request25,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

  • BREAKINGGET /health response no longer includes a nested data object. Database status is now reflected in the top-level message field ("message": "healthy"). Update any clients reading data.database or data.timestamp.

2026-05-05

  • BREAKING — Signature header renamed from X-Signature to X-Hook-Signature. Update all client requests accordingly.
  • FIXGET /health no longer requires authentication and is now publicly accessible.
  • FIXPOST /operations/{requestId}/retry was previously not handled as a POST — requests to this endpoint now work correctly.
  • FIXprice and sale_price must be positive numbers (> 0). Zero and negative values are rejected with 400.
  • FIX — GTIN changes (old_gtingtin) now correctly rename the existing product record instead of creating a duplicate.