{
  "info": {
    "_postman_id": "d4b1f9b0-6e2f-4a2a-8a1a-dawatrack-partner-api",
    "name": "DawaTrack Partner API",
    "description": "Track, trace, and recall compliance for manufacturers, distributors and pharmacies integrating with DawaTrack over HTTP. Full docs: {{docs_url}}\n\nStart with \"0. Auth > Get Access Token\" - its test script automatically saves the returned token into the `access_token` collection variable, so every other request in this collection just works afterward (they all use collection-level Bearer auth).\n\nSet `client_id`/`client_secret` in the collection variables (or in a Postman environment) before running anything. Get sandbox credentials instantly, with no approval wait, at {{sandbox_signup_url}} - they come pre-seeded with a demo manufacturer/product/batch so every read below returns real-looking data immediately. Sandbox credentials return 403 on the three requests marked \"production only\" below (see the Sandbox vs. production note in each folder).",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{access_token}}",
        "type": "string"
      }
    ]
  },
  "variable": [
    {
      "key": "base_url",
      "value": "https://api.dawatrack.com/partner-api/v1",
      "type": "string"
    },
    {
      "key": "docs_url",
      "value": "https://dawatrack.com/developers/",
      "type": "string"
    },
    {
      "key": "sandbox_signup_url",
      "value": "https://dawatrack.com/partners/sandbox/",
      "type": "string"
    },
    {
      "key": "client_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "client_secret",
      "value": "",
      "type": "string"
    },
    {
      "key": "access_token",
      "value": "",
      "type": "string"
    },
    {
      "key": "token_environment",
      "value": "",
      "type": "string"
    },
    {
      "key": "gtin",
      "value": "00000000000001",
      "type": "string"
    },
    {
      "key": "batch_number",
      "value": "SANDBOX-BATCH-001",
      "type": "string"
    },
    {
      "key": "schema_name",
      "value": "sandbox-demo-pharmacy",
      "type": "string"
    },
    {
      "key": "recall_id",
      "value": "1",
      "type": "string"
    },
    {
      "key": "webhook_id",
      "value": "1",
      "type": "string"
    }
  ],
  "item": [
    {
      "name": "0. Auth",
      "description": "OAuth2 client-credentials grant. Run \"Get Access Token\" first - everything else in this collection depends on the `access_token` variable it sets.",
      "item": [
        {
          "name": "Get Access Token",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('token request succeeded', function () {",
                  "    pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const data = pm.response.json();",
                  "if (data.access_token) {",
                  "    pm.collectionVariables.set('access_token', data.access_token);",
                  "    pm.collectionVariables.set('token_environment', data.environment);",
                  "    console.log('DawaTrack: access_token saved. environment=' + data.environment + ', scopes=' + data.scope);",
                  "} else {",
                  "    console.warn('DawaTrack: no access_token in response - check client_id/client_secret collection variables.');",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"client_id\": \"{{client_id}}\",\n  \"client_secret\": \"{{client_secret}}\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/oauth/token/",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "oauth",
                "token",
                ""
              ]
            },
            "description": "Returns a bearer token (default TTL 1 hour) plus the credential's `environment` (SANDBOX or PRODUCTION) and granted `scope`. Rate-limited to 20/hour - it's a credential-guessing target, so don't call this per-request; reuse the token until it expires."
          }
        }
      ]
    },
    {
      "name": "Products",
      "description": "The global medicine catalogue. Sandbox vs. production: reads are open to both; POST (registering/updating a product) requires a PRODUCTION credential - it writes the same shared catalogue every real tenant clones from, so sandbox writes are blocked to avoid polluting it with test data.",
      "item": [
        {
          "name": "List / search products",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/products/?search=amoxicillin",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "products",
                ""
              ],
              "query": [
                {
                  "key": "search",
                  "value": "amoxicillin"
                },
                {
                  "key": "dosage_form",
                  "value": "CAP",
                  "disabled": true
                },
                {
                  "key": "schedule",
                  "value": "OTC",
                  "disabled": true
                },
                {
                  "key": "is_controlled",
                  "value": "false",
                  "disabled": true
                },
                {
                  "key": "page",
                  "value": "1",
                  "disabled": true
                }
              ]
            },
            "description": "Requires `products:read`. `?search=` matches generic name, brand name, GTIN or NDC code."
          }
        },
        {
          "name": "Get product by GTIN",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/products/{{gtin}}/",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "products",
                "{{gtin}}",
                ""
              ]
            },
            "description": "Requires `products:read`. Accepts an 8/12/13/14-digit GTIN - normalized internally."
          }
        },
        {
          "name": "Register/update a product (production only)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"gtin\": \"{{gtin}}\",\n  \"generic_name\": \"Amoxicillin\",\n  \"strength\": \"500mg\",\n  \"dosage_form\": \"CAP\",\n  \"schedule\": \"OTC\",\n  \"is_controlled\": false\n}"
            },
            "url": {
              "raw": "{{base_url}}/products/",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "products",
                ""
              ]
            },
            "description": "Requires `products:write` AND a PRODUCTION credential - returns 403 `{\"detail\": \"This action requires a production credential. Sandbox credentials are read + drill only.\"}` on a SANDBOX token. Matched first by GTIN, then by (generic_name, strength, dosage_form). You may only create a new product or update one your own credential already owns."
          }
        }
      ]
    },
    {
      "name": "Facilities",
      "item": [
        {
          "name": "Verify facility license number",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/facilities/verify/?license_number=BU202608770",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "facilities",
                "verify",
                ""
              ],
              "query": [
                {
                  "key": "license_number",
                  "value": "BU202608770"
                }
              ]
            },
            "description": "Requires `facilities:read`. Checks a license number against PPB's real government facility register (not just DawaTrack's own supplier/pharmacy tables). Always 200 with found_in_ppb_register: true/false - never a 404 for 'not found in PPB', since that's itself a valid answer. Also cross-references is_dawatrack_supplier/is_dawatrack_pharmacy."
          }
        }
      ]
    },
    {
      "name": "Batches",
      "description": "Your distribution ledger. Sandbox vs. production: reads are open to both (your sandbox credential already has one seeded demo batch to read); POST (confirming a real shipment into a real facility's inventory) requires a PRODUCTION credential.",
      "item": [
        {
          "name": "List my batch distributions",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/batches/",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "batches",
                ""
              ]
            },
            "description": "Requires `batches:read`. A sandbox manufacturer credential returns its one seeded demo batch."
          }
        },
        {
          "name": "Trace a batch (gtin + batch_number)",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/batches/{{gtin}}/{{batch_number}}/",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "batches",
                "{{gtin}}",
                "{{batch_number}}",
                ""
              ]
            },
            "description": "Requires `batches:read`. Full distribution history for one lot - the \"where did this batch go\" lookup a recall investigation starts from."
          }
        },
        {
          "name": "Confirm a batch shipment (production only)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"gtin\": \"{{gtin}}\",\n  \"batch_number\": \"{{batch_number}}\",\n  \"quantity\": 500,\n  \"schema_name\": \"{{schema_name}}\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/batches/",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "batches",
                ""
              ]
            },
            "description": "Requires `batches:write`, manufacturer partner type, AND a PRODUCTION credential. The batch must already exist in the named facility's own inventory - this endpoint confirms a real delivery, it doesn't fabricate one. Accepts `gs1_element` (a raw scanned GS1 DataMatrix string) instead of `gtin`/`batch_number` if you have one. `Idempotency-Key` is a Postman dynamic variable here (`{{$guid}}`) so re-sending this exact request in the console doesn't create a second row - generate your own key per real attempt in production code."
          }
        }
      ]
    },
    {
      "name": "Events",
      "description": "Append-only supply-chain log - always safe, works identically in sandbox and production (it's scoped to your own credential, never touches shared or real facility state).",
      "item": [
        {
          "name": "Log a supply-chain event",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"event_type\": \"SHIP\",\n  \"gtin\": \"{{gtin}}\",\n  \"batch_number\": \"{{batch_number}}\",\n  \"occurred_at\": \"2026-08-20T09:00:00Z\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/events/",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "events",
                ""
              ]
            },
            "description": "Requires `events:write`. `event_type` is one of COMMISSION, SHIP, RECEIVE, DISPENSE, QUARANTINE (not AGGREGATE - see the Aggregations folder). `occurred_at` defaults to now if omitted. Works on a SANDBOX credential. A COMMISSION event requires `serial_number` and must be the first-ever COMMISSION for this (gtin, serial) pair - PPB's AI (21) serial is unique per GTIN for the life of the unit, never reused."
          }
        },
        {
          "name": "Commission a serialized unit",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"event_type\": \"COMMISSION\",\n  \"gtin\": \"{{gtin}}\",\n  \"batch_number\": \"{{batch_number}}\",\n  \"serial_number\": \"SN-0001\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/events/",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "events",
                ""
              ]
            },
            "description": "The \"this physical unit now exists\" event - the per-unit ledger's entry point. See GET /units/{gtin}/{serial}/ below to read it back."
          }
        },
        {
          "name": "Get a unit's status/history",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/units/{{gtin}}/SN-0001/",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "units",
                "{{gtin}}",
                "SN-0001",
                ""
              ]
            },
            "description": "Requires `batches:read`. Full custody trail for one physical unit (newest first) plus current status. 404 if this exact (gtin, serial) has never been commissioned, or isn't visible to this credential (same visibility rule as GET /batches/{gtin}/{batch_number}/ - a manufacturer sees what it makes, a distributor/pharmacy sees what a linked facility has received)."
          }
        }
      ]
    },
    {
      "name": "Recalls",
      "description": "Sandbox vs. production: reads, acknowledgment, and drills work on both; opening a REAL recall (`POST /recalls/`) requires a PRODUCTION credential, since it fans out background jobs into a facility's real inventory. Run the drill request against your sandbox credential's seeded demo batch to see the same Classify+Locate logic with zero real-world effect.",
      "item": [
        {
          "name": "List recalls",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/recalls/",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "recalls",
                ""
              ]
            },
            "description": "Requires `recalls:read`. Manufacturer: recalls you opened. Distributor/pharmacy: recalls affecting a facility you're linked to."
          }
        },
        {
          "name": "Get recall detail",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/recalls/{{recall_id}}/",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "recalls",
                "{{recall_id}}",
                ""
              ]
            },
            "description": "Requires `recalls:read`. Includes PPB deadlines (`progress_report_due_at`, `reconciliation_due_at`) and acknowledgment progress."
          }
        },
        {
          "name": "Run a recall drill (sandbox-safe)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"batch_number\": \"{{batch_number}}\",\n  \"reason\": \"QUALITY_DEFECT\",\n  \"severity_class\": \"CLASS_II\",\n  \"instructions\": \"DRILL - no action required.\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/recalls/drill/",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "recalls",
                "drill",
                ""
              ]
            },
            "description": "Requires `recalls:write`, manufacturer partner type. Works on SANDBOX - counts facilities a real recall would reach but notifies nobody, quarantines nothing. This is the request to run against your sandbox credential's seeded demo batch (default `batch_number` variable above)."
          }
        },
        {
          "name": "Open a real recall (production only)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"batch_number\": \"{{batch_number}}\",\n  \"reason\": \"CONTAMINATION\",\n  \"severity_class\": \"CLASS_II\",\n  \"instructions\": \"Quarantine and await collection instructions.\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/recalls/",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "recalls",
                ""
              ]
            },
            "description": "Requires `recalls:write`, manufacturer partner type, AND a PRODUCTION credential - notifies and quarantines stock at every real facility that received this batch before the response returns. `reason`: QUALITY_DEFECT, CONTAMINATION, MISLABELING, ADVERSE_REACTION, REGULATORY_WITHDRAWAL, OTHER. `severity_class`: CLASS_I/II/III per PPB Guidelines Section 7."
          }
        },
        {
          "name": "Acknowledge a recall notice",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"schema_name\": \"{{schema_name}}\",\n  \"notes\": \"Stock pulled from shelf.\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/recalls/{{recall_id}}/acknowledge/",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "recalls",
                "{{recall_id}}",
                "acknowledge",
                ""
              ]
            },
            "description": "Requires `recalls:acknowledge` (distributor/pharmacy). `schema_name` can be omitted if your credential is linked to exactly one facility. Works on a SANDBOX credential against the seeded demo facility link."
          }
        },
        {
          "name": "Get PPB compliance report",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/recalls/{{recall_id}}/report/?stage=initial",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "recalls",
                "{{recall_id}}",
                "report",
                ""
              ],
              "query": [
                {
                  "key": "stage",
                  "value": "initial"
                }
              ]
            },
            "description": "Requires `recalls:read`, manufacturer only. `stage` is one of `initial` (1 week), `follow_up` (2 weeks), `final` (4 weeks) per PPB Guidelines \u00a72.13.3."
          }
        }
      ]
    },
    {
      "name": "Public (no credential)",
      "description": "The one endpoint in this API that needs no authentication at all.",
      "item": [
        {
          "name": "Public recall lookup",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/recall-lookup/{{gtin}}/{{batch_number}}/",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "recall-lookup",
                "{{gtin}}",
                "{{batch_number}}",
                ""
              ]
            },
            "description": "\"Has this batch been recalled?\" - free, IP-rate-limited (60/hour), meant to be embedded in a consumer app or a pharmacy's own dispense flow. Never requires a token. Matches on {gtin} and {batch_number} together, not batch_number alone - a batch/lot number is assigned independently by each manufacturer, so two unrelated products can legitimately share the same batch_number text."
          }
        }
      ]
    },
    {
      "name": "Webhooks",
      "description": "Push instead of polling GET /recalls/. Fully functional on a SANDBOX credential - deliveries only ever go to your own target_url.",
      "item": [
        {
          "name": "List my webhook subscriptions",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/webhooks/",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "webhooks",
                ""
              ]
            },
            "description": "Requires `webhooks:manage`."
          }
        },
        {
          "name": "Register a webhook",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"target_url\": \"https://webhook.site/your-unique-url\",\n  \"event_types\": [\"recall.issued\", \"recall.escalated\", \"hold.lifted\"]\n}"
            },
            "url": {
              "raw": "{{base_url}}/webhooks/",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "webhooks",
                ""
              ]
            },
            "description": "Requires `webhooks:manage`. The response includes a `secret` used to verify the `X-Dawatrack-Signature` header on each delivery - save it, it's not shown again. Tip: point `target_url` at https://webhook.site while testing to see raw deliveries without standing up your own receiver."
          }
        },
        {
          "name": "Delete a webhook subscription",
          "request": {
            "method": "DELETE",
            "url": {
              "raw": "{{base_url}}/webhooks/{{webhook_id}}/",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "webhooks",
                "{{webhook_id}}",
                ""
              ]
            },
            "description": "Requires `webhooks:manage`. Only your own subscription."
          }
        },
        {
          "name": "List delivery attempts",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/webhooks/{{webhook_id}}/deliveries/",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "webhooks",
                "{{webhook_id}}",
                "deliveries",
                ""
              ]
            },
            "description": "Requires `webhooks:manage`. Shows every delivery attempt including ones that reached FAILED after retries were exhausted."
          }
        }
      ]
    },
    {
      "name": "Aggregations (SSCC)",
      "description": "Capture-and-forward only, manufacturer-only writes - Dawatrack records what a manufacturer declares is packed under an SSCC (GS1 AI (00)) and never independently re-verifies contents. PRIMS is the verification authority for aggregation integrity per PPB's standard; every child unit must already have been commissioned (see the Events folder) before it can be aggregated.",
      "item": [
        {
          "name": "Declare an SSCC's contents",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"sscc\": \"106141412345678908\",\n  \"children\": [\n    { \"gtin\": \"{{gtin}}\", \"serial\": \"SN-0001\" }\n  ]\n}"
            },
            "url": {
              "raw": "{{base_url}}/aggregations/",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "aggregations",
                ""
              ]
            },
            "description": "Requires `events:write`, manufacturer partner type. `sscc` must be a valid 18-digit GS1 SSCC (check digit verified). Every `children` entry must already have a COMMISSION event on record (run \"Commission a serialized unit\" first) - 404 if any haven't been. The example SSCC here is a real, published GS1 example value."
          }
        },
        {
          "name": "Get an SSCC's declared contents",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/aggregations/106141412345678908/",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "aggregations",
                "106141412345678908",
                ""
              ]
            },
            "description": "Requires `batches:read`. Not scoped to the credential that created it - knowing the exact SSCC is the access control here, the same posture as the public recall lookup's exact-GTIN+batch requirement, since the whole point is a downstream distributor/pharmacy reading what the manufacturer declared."
          }
        }
      ]
    }
  ]
}
