External API v1

ZenAdmin API

Programmatic access to your company-scoped external APIs — inventory devices, hardware catalog, employee directory, order submission, and outbound webhooks for lifecycle updates.

Base URL

https://prod-api.zenadmin.ai/api/external

Content Type

All requests and responses use application/json.

Timestamps

ISO-8601 UTC (e.g. 2026-05-08T15:21:09.000Z).

Example Request
curl -X GET "https://prod-api.zenadmin.ai/api/external/me" \
  -H "x-api-key: your-api-key-here"

Quick Start

Follow this checklist to go from zero to a working automation — no UI required beyond your secret store and curl.

  1. Request an API key from your ZenAdmin account team. Keys are provisioned by ZenAdmin ops; the plaintext is shown once at creation. Store it in your secret manager immediately.
  2. Verify connectivity with GET /me (see curl below).
  3. Browse the catalog: GET /catalog/countries → pick a country_idGET /catalog/devices?country_id=....
  4. Create a test order: POST /orders with a unique external_request_id.
  5. Register webhooks: POST /webhooks/subscriptions — save the returned secret (shown once).
  6. Monitor deliveries: GET /webhooks/deliveries?subscription_id=... to confirm ZenAdmin reached your endpoint.
  7. Retry a failed delivery: POST /webhooks/deliveries/:id/redeliver (returns 202).
Step 2 — Verify your API key
Step 5 — Create a webhook subscription
Step 6 — List delivery logs
GET /me
curl -X GET "https://prod-api.zenadmin.ai/api/external/me" \
  -H "x-api-key: YOUR_KEY"
POST /webhooks/subscriptions
curl -X POST "https://prod-api.zenadmin.ai/api/external/webhooks/subscriptions" \
  -H "x-api-key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-company.com/webhooks/zenadmin",
    "events": ["order.status_changed", "rfq.created"]
  }'
GET /webhooks/deliveries
curl -X GET "https://prod-api.zenadmin.ai/api/external/webhooks/deliveries?subscription_id=SUB_UUID&status=failed" \
  -H "x-api-key: YOUR_KEY"

Authentication

All API requests require a valid company-scoped API key provisioned by ZenAdmin.

Obtaining an API key

Contact your ZenAdmin account team to request API access. ZenAdmin ops will create a key for your company. The plaintext key is displayed only once at creation — copy it to your secret manager before closing the provisioning screen.

MethodHeaderValue
Recommendedx-api-keyyour-api-key
AlternativeAuthorizationBearer your-api-key
Rate limits
ScopeLimitBurst
Global (all endpoints)600 req/min per key1200
POST /orders only60 req/min per key120
Important: A revoked or expired key returns 401. There is no OAuth — API keys only.
401 Response
{
  "success": false,
  "message": "Invalid or expired API key"
}
429 Response
{
  "success": false,
  "message": "Rate limit exceeded",
  "errorType": "rate_limited"
}

Error Handling

Errors return success: false with a human-readable message. Many errors also include errorType and optional data with field-level details.

StatusMeaningWhen
200OKRequest succeeded (includes idempotent order replay)
201CreatedNew resource created (e.g. order)
202AcceptedAsync action queued (e.g. webhook redelivery)
400Bad RequestValidation error or missing required field
401UnauthorizedInvalid or expired API key
404Not FoundResource not found
409ConflictBusiness rule violation (not cancellable, duplicate subscription)
422UnprocessableInvalid domain value (country, catalog item)
429Too Many RequestsRate limit exceeded
Common errorType values
errorTypeStatusWhen
employee_not_found404No employee matched user_id / work_email / employee_id
employee_address_missing409ship_to_type=employee but no personal address on file
invalid_country_id422Unknown address.country_id
invalid_catalog_item422Unknown product_configuration_id
missing_external_request_id400Idempotency key omitted from order create
order_not_found404RFQ not found for this company
order_not_cancellable409Order already in_transit / delivered / completed
duplicate_subscription409Webhook URL already registered
subscription_not_found404Webhook subscription ID not found
delivery_not_found404Webhook delivery ID not found
catalog_item_not_found404Catalog device not found
rate_limited429Per-key rate limit hit
Error Envelope
{
  "success": false,
  "message": "No employee matched work_email",
  "errorType": "employee_not_found",
  "data": {
    "field": "work_email",
    "value": "unknown@company.com"
  }
}
Validation Error
{
  "success": false,
  "message": "Data validation failed"
}

Pagination

List endpoints accept page (0-based) and limit (max 100, default 25). Responses include a meta_data object.

FieldDescription
total_itemsTotal matching records
page_noCurrent page (0-based)
items_on_pageRecords in this response
total_pagesTotal pages (catalog, employees, orders, webhook deliveries)
has_next_pagetrue if more pages exist
has_prev_pagetrue if page > 0
Note: GET /devices returns only total_items, page_no, and items_on_page — not the extended pagination fields.
meta_data Example
{
  "meta_data": {
    "total_items": 142,
    "page_no": 0,
    "items_on_page": 25,
    "total_pages": 6,
    "has_next_page": true,
    "has_prev_page": false
  }
}

List Devices

GET/api/external/devices

Returns company-scoped inventory devices.

Query Parameters
ParameterTypeDescription
pageintegerPage number (default 0)
limitintegerPage size, max 100 (default 25)
searchstringFree-text search
sort_bystringSort field
filter_statusstringFilter by device status
filter_typeuuidFilter by product type ID
filter_branduuidFilter by brand ID
Request
curl -X GET "https://prod-api.zenadmin.ai/api/external/devices?page=0&limit=25&search=MacBook" \
  -H "x-api-key: YOUR_KEY"
200 Response
{
  "success": true,
  "data": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "name": "MacBook Pro 14\"",
      "status": "allocated",
      "asset_tag": "ZN-00421",
      "serial_number": "C02XYZ123456",
      "device_grading": "A",
      "product_type": "Laptop",
      "brand": "Apple",
      "model": "MacBook Pro 14",
      "ram": "16 GB",
      "storage": "512 GB",
      "processor": "M3 Pro",
      "keyboard_language": "US",
      "img_url": "https://assets.zenadmin.ai/...",
      "location": { "city": "Singapore", "country": "Singapore" },
      "allocated_to": {
        "id": "user-uuid",
        "first_name": "Alice",
        "middle_name": null,
        "last_name": "Chen",
        "work_email": "alice@acme.com"
      },
      "procurement_type": "purchase",
      "purchase_date": "2024-03-15",
      "warranty_end_date": "2027-03-15",
      "mdm": "Jamf",
      "metadata": {}
    }
  ],
  "meta_data": {
    "total_items": 48,
    "page_no": 0,
    "items_on_page": 25
  }
}

Get Device Details

GET/api/external/devices/:id

Returns full details for a single inventory device by UUID.

Path Parameters
ParameterTypeDescription
iduuidDevice ID
Request
curl -X GET "https://prod-api.zenadmin.ai/api/external/devices/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
  -H "x-api-key: YOUR_KEY"
200 Response
{
  "success": true,
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "name": "MacBook Pro 14\"",
    "status": "allocated",
    "asset_tag": "ZN-00421",
    "serial_number": "C02XYZ123456",
    "product_type": "Laptop",
    "brand": "Apple",
    "model": "MacBook Pro 14",
    "ram": "16 GB",
    "storage": "512 GB",
    "processor": "M3 Pro",
    "allocated_to": {
      "id": "user-uuid",
      "first_name": "Alice",
      "last_name": "Chen",
      "work_email": "alice@acme.com"
    }
  }
}
404 Response
{
  "success": false,
  "message": "Device not found"
}

Get API Context

GET/api/external/me

Returns your company_id, API version, and the full list of supported webhook events. Use this as a connectivity test after receiving your API key.

Request
curl -X GET "https://prod-api.zenadmin.ai/api/external/me" \
  -H "x-api-key: YOUR_KEY"
200 Response
{
  "success": true,
  "data": {
    "company_id": "99fe5378-d0da-4735-9167-0ba35578e168",
    "api_version": "v1",
    "supported_events": [
      "rfq.created",
      "rfq.cancelled",
      "rfq.status_changed",
      "order.placed",
      "order.confirmed",
      "order.in_transit",
      "order.delivered",
      "order.cancelled",
      "order.tracking_added",
      "order.status_changed",
      "inventory.archived",
      "inventory.restored",
      "inventory.archive_updated",
      "disposal.requested"
    ]
  }
}

Create Order

POST/api/external/orders
Key points
  • Idempotent by external_request_id — replays return 200
  • Employee resolution: user_idwork_emailemployee_id
  • external_system: jira | freshservice | atlassian | zendesk | servicenow | custom
  • ship_to_type: employee (personal address) or address (custom block)
  • Rate limit: 60 req/min (burst 120) on top of global limit
Request Body Fields
FieldRequiredDescription
external_request_idYesIdempotency key (unique per order attempt)
external_systemYesSource system identifier
external_ticket_idYesTicket/issue ID in source system
external_urlNoLink back to source ticket
employeeYesOne of user_id, work_email, employee_id
ship_to_typeYesemployee or address
addressIf addressline1, city, country_id, postal_code, contact_number
itemsYesArray of { product_configuration_id, quantity? }
notesNoFree-text notes
Request Body
{
  "external_request_id": "JIRA-AUTOMATION-2026-05-08-1A2B3C",
  "external_system": "jira",
  "external_ticket_id": "DEV-1234",
  "external_url": "https://acme.atlassian.net/browse/DEV-1234",
  "employee": {
    "work_email": "alice@acme.com"
  },
  "ship_to_type": "address",
  "address": {
    "line1": "1 Infinite Loop",
    "city": "Cupertino",
    "country_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "postal_code": "95014",
    "contact_number": "+15551234567"
  },
  "items": [
    {
      "product_configuration_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "quantity": 1
    }
  ],
  "notes": "Standard new-hire kit"
}
201 Response
{
  "success": true,
  "message": "Order created",
  "data": {
    "rfq_id": 105821,
    "id": "8d8f5b39-1a2b-3c4d-5e6f-789012345678",
    "status": "sent",
    "external_request_id": "JIRA-AUTOMATION-2026-05-08-1A2B3C",
    "external_system": "jira",
    "external_ticket_id": "DEV-1234",
    "external_url": "https://acme.atlassian.net/browse/DEV-1234",
    "company_id": "99fe5378-d0da-4735-9167-0ba35578e168",
    "source": "external",
    "created_at": "2026-05-08T10:00:00.000Z",
    "items": [
      {
        "product_type": "Laptop",
        "brand": "Apple",
        "model": "MacBook Pro 14",
        "quantity": 1,
        "order_id": null
      }
    ],
    "order": null
  }
}
200 Response (duplicate)
{
  "success": true,
  "message": "Order already exists for this external_request_id",
  "data": { "...same rfq payload..." }
}

Get Order

GET/api/external/orders/:rfq_id

Returns the full RFQ plus derived order items and per-item tracking history. Accepts numeric RFQ ID or internal UUID.

Request
curl -X GET "https://prod-api.zenadmin.ai/api/external/orders/105821" \
  -H "x-api-key: YOUR_KEY"
200 Response
{
  "success": true,
  "data": {
    "rfq_id": 105821,
    "id": "8d8f5b39-1a2b-3c4d-5e6f-789012345678",
    "status": "in_progress",
    "external_request_id": "JIRA-AUTOMATION-2026-05-08-1A2B3C",
    "external_system": "jira",
    "external_ticket_id": "DEV-1234",
    "company_id": "99fe5378-d0da-4735-9167-0ba35578e168",
    "requested_by": {
      "id": "user-uuid",
      "first_name": "Alice",
      "last_name": "Chen",
      "email": "alice@acme.com"
    },
    "shipping": {
      "city": "Cupertino",
      "country": "United States",
      "postal_code": "95014",
      "address_line1": "1 Infinite Loop"
    },
    "items": [
      {
        "product_type": "Laptop",
        "brand": "Apple",
        "model": "MacBook Pro 14",
        "quantity": 1,
        "order_id": "ORD-29381"
      }
    ],
    "order": {
      "order_id": "ORD-29381",
      "items": [
        {
          "id": "order-item-uuid",
          "status": "in_transit",
          "delivery_at": "2026-05-12T09:00:00.000Z",
          "shipment_tracking_number": "1Z999AA10123456784",
          "tracking": [
            {
              "status": "confirmed",
              "status_date": "2026-05-09T08:00:00.000Z"
            },
            {
              "status": "in_transit",
              "status_date": "2026-05-10T14:30:00.000Z"
            }
          ]
        }
      ]
    }
  }
}

List Orders

GET/api/external/orders
Query Parameters
ParameterTypeDescription
pageintegerPage number (default 0)
limitintegerMax 100 (default 25)
statusstringsent | in_progress | completed | expired
external_systemstringFilter by source system
external_ticket_idstringFilter by ticket ID
fromISO dateCreated on or after
toISO dateCreated on or before
Request
curl -X GET "https://prod-api.zenadmin.ai/api/external/orders?status=in_progress&page=0&limit=25" \
  -H "x-api-key: YOUR_KEY"
200 Response
{
  "success": true,
  "data": [
    {
      "rfq_id": 105821,
      "id": "8d8f5b39-1a2b-3c4d-5e6f-789012345678",
      "status": "in_progress",
      "external_ticket_id": "DEV-1234",
      "external_system": "jira",
      "created_at": "2026-05-08T10:00:00.000Z",
      "items": [{ "product_type": "Laptop", "brand": "Apple", "model": "MacBook Pro 14", "quantity": 1 }]
    }
  ],
  "meta_data": {
    "total_items": 3,
    "page_no": 0,
    "items_on_page": 3,
    "total_pages": 1,
    "has_next_page": false,
    "has_prev_page": false
  }
}

Cancel Order

POST/api/external/orders/:rfq_id/cancel

Cancels an RFQ. Emits rfq.cancelled webhook on success.

Returns 409 order_not_cancellable if any order item has reached in_transit or delivered, or the RFQ is already completed.
Request Body (optional)
FieldTypeDescription
reasonstringCancellation reason (included in webhook payload)
Request
curl -X POST "https://prod-api.zenadmin.ai/api/external/orders/105821/cancel" \
  -H "x-api-key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Employee no longer joining"}'
409 Response
{
  "success": false,
  "message": "Order cannot be cancelled in its current state",
  "errorType": "order_not_cancellable"
}

List Catalog Devices

GET/api/external/catalog/devices

Browse orderable hardware for a country. Global catalog — not scoped to a department.

Query Parameters
ParameterTypeDescription
country_iduuidRequired. Country for pricing/availability
searchstringSearch by product name
product_typeuuidFilter by product type ID
branduuidFilter by brand ID
pageintegerPage number (default 0)
limitintegerMax 100 (default 25)
Request
curl -X GET "https://prod-api.zenadmin.ai/api/external/catalog/devices?country_id=f47ac10b-58cc-4372-a567-0e02b2c3d479&search=MacBook&page=0&limit=25" \
  -H "x-api-key: YOUR_KEY"
200 Response
{
  "success": true,
  "data": [
    {
      "product_configuration_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "thumbnail": "https://assets.zenadmin.ai/...",
      "name": "MacBook Pro 14\"",
      "description": "M3 Pro, 16 GB RAM, 512 GB Storage, 14 inch Display",
      "price": 2499.00,
      "product_type": "Laptop",
      "product_type_id": "type-uuid",
      "brand": "Apple",
      "brand_id": "brand-uuid",
      "processor": "M3 Pro",
      "ram": "16 GB",
      "storage": "512 GB",
      "display": "14",
      "color": "Space Gray"
    }
  ],
  "meta_data": {
    "total_items": 12,
    "page_no": 0,
    "items_on_page": 12,
    "total_pages": 1,
    "has_next_page": false,
    "has_prev_page": false
  }
}

Get Catalog Device

GET/api/external/catalog/devices/:product_configuration_id
Query Parameters
ParameterTypeDescription
country_iduuidOptional — include country-specific price
Request
curl -X GET "https://prod-api.zenadmin.ai/api/external/catalog/devices/a1b2c3d4-e5f6-7890-abcd-ef1234567890?country_id=f47ac10b-58cc-4372-a567-0e02b2c3d479" \
  -H "x-api-key: YOUR_KEY"
200 Response
{
  "success": true,
  "data": {
    "product_configuration_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "thumbnail": "https://assets.zenadmin.ai/...",
    "name": "MacBook Pro 14\"",
    "description": "Apple MacBook Pro 14-inch",
    "price": 2499.00,
    "product_type": "Laptop",
    "brand": "Apple",
    "processor": "M3 Pro",
    "ram": "16 GB",
    "storage": "512 GB",
    "display": "14",
    "color": "Space Gray"
  }
}
404 Response
{
  "success": false,
  "message": "Catalog item not found",
  "errorType": "catalog_item_not_found"
}

List Countries

GET/api/external/catalog/countries

Returns all countries for catalog lookups. Use the id as country_id in catalog device queries.

Request
curl -X GET "https://prod-api.zenadmin.ai/api/external/catalog/countries" \
  -H "x-api-key: YOUR_KEY"
200 Response
{
  "success": true,
  "data": [
    { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "name": "United States", "country_code": "US" },
    { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Singapore", "country_code": "SG" }
  ]
}

List Employees

GET/api/external/employees

Lightweight employee directory for picker UIs and automations.

Query Parameters
ParameterTypeDescription
searchstringSearch name, email, or employee ID
statusstringnew | active | offboarding | offboarded | deleted
department_iduuidFilter by department
pageintegerPage number (default 0)
limitintegerMax 100 (default 25)
Request
curl -X GET "https://prod-api.zenadmin.ai/api/external/employees?search=alice&status=active&page=0&limit=25" \
  -H "x-api-key: YOUR_KEY"
200 Response
{
  "success": true,
  "data": [
    {
      "id": "user-uuid",
      "first_name": "Alice",
      "middle_name": null,
      "last_name": "Chen",
      "work_email": "alice@acme.com",
      "email": "alice@acme.com",
      "employee_id": "EMP-0042",
      "status": "active",
      "start_date": "2024-01-15",
      "last_working_date": null,
      "phone_number": "+15551234567",
      "profile_img_url": null,
      "department_id": "dept-uuid",
      "department_name": "Engineering",
      "position_id": "pos-uuid",
      "position_name": "Software Engineer",
      "reporting_manager_id": "mgr-uuid",
      "reporting_manager_first_name": "Bob",
      "reporting_manager_last_name": "Smith",
      "reporting_manager_work_email": "bob@acme.com"
    }
  ],
  "meta_data": {
    "total_items": 1,
    "page_no": 0,
    "items_on_page": 1,
    "total_pages": 1,
    "has_next_page": false,
    "has_prev_page": false
  }
}

Get Employee

GET/api/external/employees/:id

Returns a single employee by user UUID.

Request
curl -X GET "https://prod-api.zenadmin.ai/api/external/employees/user-uuid" \
  -H "x-api-key: YOUR_KEY"
404 Response
{
  "success": false,
  "message": "Employee not found",
  "errorType": "employee_not_found"
}

List Markets

GET/api/external/webstore/countries

Countries the webstore catalogue can be priced for. Use the id as country_id in the product endpoints below.

Query Parameters
ParameterTypeDescription
searchstringMatches the country name
pageintegerPage number (default 0)
limitintegerPage size, max 100 (default 25)
Request
curl -X GET "https://prod-api.zenadmin.ai/api/external/webstore/countries?search=sing" \
  -H "x-api-key: YOUR_KEY"
200 Response
{
  "success": true,
  "data": [
    { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Singapore", "country_code": "SG" }
  ],
  "meta_data": {
    "total_items": 249,
    "page_no": 0,
    "items_on_page": 1,
    "total_pages": 10,
    "has_next_page": true,
    "has_prev_page": false
  }
}

List Product Filters

GET/api/external/webstore/products/filters

Takes no parameters. Returns every valid value accepted by the spec filters on /webstore/products — brands, product types, operating systems, processor, RAM, storage and screen size. Always read filter values from here rather than constructing them, as the stored values are matched exactly.

Request
curl -X GET "https://prod-api.zenadmin.ai/api/external/webstore/products/filters" \
  -H "x-api-key: YOUR_KEY"
200 Response
{
  "success": true,
  "data": {
    "header": [
      {
        "key": "filter_product_type",
        "name": "All Products",
        "options": [
          { "value": "73f98036-80a1-469e-b855-d08c8488ef22", "name": "Laptops" }
        ]
      }
    ],
    "drawer": [
      {
        "key": "filter_ram",
        "name": "RAM",
        "options": [
          { "value": "16GB", "name": "16 GB" },
          { "value": "32GB", "name": "32 GB" }
        ]
      }
    ]
  }
}

List Webstore Products

GET/api/external/webstore/products

The full orderable catalogue, broader than /catalog/devices — it spans every product type, not only laptops.

country_id selects the market a product is priced for; it does not filter the list. Products with no price in that market are still returned with price: null and remain requestable — ops supply the figure when converting the RFQ.

Query Parameters
ParameterTypeDescription
country_iduuidRequired. Market to price for. Unknown values return 422 invalid_country_id
searchstringMatches product name, brand and product type — not country
product_typeuuidComma-separated product type IDs
branduuidComma-separated brand IDs
osuuidComma-separated operating system IDs
processorstringValue from /products/filters
ramstringValue from /products/filters, matched exactly
storagestringValue from /products/filters
displaystringValue from /products/filters
sortstringprice-asc or price-desc
pageintegerPage number (default 0)
limitintegerPage size, max 100 (default 25)
Request
curl -X GET "https://prod-api.zenadmin.ai/api/external/webstore/products?country_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890&ram=16GB" \
  -H "x-api-key: YOUR_KEY"
200 Response
{
  "success": true,
  "data": [
    {
      "id": "977004f4-7920-4d7e-9c51-fe9783afec13",
      "variant_id": "aa11bb22-cc33-4d44-9e55-ff6677889900",
      "name": "Apple MacBook Pro 14\"",
      "product_company": "Apple",
      "product_type": "Laptops",
      "price": "2399.00",
      "country": "Singapore"
    }
  ],
  "meta_data": {
    "total_items": 237,
    "page_no": 0,
    "items_on_page": 1,
    "total_pages": 10,
    "has_next_page": true,
    "has_prev_page": false
  }
}

Get Webstore Product

GET/api/external/webstore/products/:id

One product with every configuration it is sold in.

Ordering: the value POST /orders expects as product_configuration_id is variants[].id here, or variant_id in the list response. The top-level id is the product, not the configuration — sending it returns 422 invalid_catalog_item.

Query Parameters
ParameterTypeDescription
country_iduuidRequired. Market to price for
Request
curl -X GET "https://prod-api.zenadmin.ai/api/external/webstore/products/977004f4-7920-4d7e-9c51-fe9783afec13?country_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
  -H "x-api-key: YOUR_KEY"
200 Response
{
  "success": true,
  "data": {
    "id": "977004f4-7920-4d7e-9c51-fe9783afec13",
    "name": "Apple MacBook Pro 14\"",
    "product_company": "Apple",
    "product_type": "Laptops",
    "variants": [
      {
        "id": "aa11bb22-cc33-4d44-9e55-ff6677889900",
        "ram": "16GB",
        "storage": "512GB",
        "price": "2399.00"
      }
    ]
  }
}

List Retirement Reasons

GET/api/external/inventories/archive-reasons

Reasons a device can be retired for. The id is required when archiving.

sold and waste_disposal are disposal reasons. Setting any other reason clears the six disposal answers on the record.

Query Parameters
ParameterTypeDescription
searchstringMatches the reason label
Request
curl -X GET "https://prod-api.zenadmin.ai/api/external/inventories/archive-reasons" \
  -H "x-api-key: YOUR_KEY"
200 Response
{
  "success": true,
  "data": [
    { "id": "c893374e-d530-4943-a8f2-84c384cec0a5", "value": "sold", "label": "Sold" },
    { "id": "35a10375-fb0d-4a92-b81b-a78266b5b038", "value": "lost", "label": "Lost" }
  ]
}

Retire Devices

POST/api/external/inventories/archived

Retires up to 50 devices, recording why, the sale price and certificate, and whether ZenAdmin should collect and dispose of them.

No user field. There is deliberately no user_id in this API — an API key identifies a company, not a person. The action is attributed server-side to the company's administrator, and sending user_id returns 400.

Side effects: retiring an allocated device deallocates it (no employee email is sent), and want_zenadmin_to_dispose: true raises a disposal request with ZenAdmin operations.

Body Parameters
ParameterTypeDescription
archivearrayRequired. 1–50 devices
archive[].iduuidRequired. Device ID
archive[].reasonuuidID from /archive-reasons
archive[].notesstringFree text, max 2000
archive[].sale_pricestringAmount realised, if sold
archive[].certificate_numberstringData-wipe certificate reference
archive[].want_zenadmin_to_disposebooleanAsk ZenAdmin to collect and dispose
archive[].need_data_wipe_certificatebooleanCertificate required on disposal
archive[].has_original_invoicebooleanOriginal invoice available
archive[].is_apple_id_lockedbooleanDevice still tied to an Apple ID
archive[].can_release_from_abmbooleanReleasable from Apple Business Manager
archive[].can_release_from_autopilotbooleanReleasable from Windows Autopilot
archive[].archive_documentsarrayDocuments to attach — see Upload Document
Request
curl -X POST "https://prod-api.zenadmin.ai/api/external/inventories/archived" \
  -H "x-api-key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "archive": [{
      "id": "4f531f64-7872-403f-b115-3f33f42fd5a7",
      "reason": "c893374e-d530-4943-a8f2-84c384cec0a5",
      "notes": "End of lease",
      "sale_price": "450",
      "certificate_number": "WIPE-88213",
      "want_zenadmin_to_dispose": true,
      "need_data_wipe_certificate": true
    }]
  }'
200 Response
{
  "success": true,
  "message": "Archived successfully"
}

List Retired Devices

GET/api/external/inventories/archived

Devices that have already been retired, with their reason and disposal details. Retired devices are excluded from GET /devices.

Query Parameters
ParameterTypeDescription
searchstringFree-text search
sort_bystringSort field
sort_orderstringASC or DESC (default ASC)
pageintegerPage number (default 0)
limitintegerPage size, max 100 (default 25)
Request
curl -X GET "https://prod-api.zenadmin.ai/api/external/inventories/archived?sort_order=DESC" \
  -H "x-api-key: YOUR_KEY"
200 Response
{
  "success": true,
  "data": [
    {
      "id": "4f531f64-7872-403f-b115-3f33f42fd5a7",
      "serial_number": "QA-20260910-RFQ796",
      "status": "archived"
    }
  ],
  "meta_data": {
    "total_items": 62,
    "page_no": 0,
    "items_on_page": 1,
    "total_pages": 3,
    "has_next_page": true,
    "has_prev_page": false
  }
}

Get Retirement Record

GET/api/external/inventories/archived/:id

The full retirement record: reason, notes, sale price, certificate, attached documents, and who retired the device and when.

additional_info.archived_by names a real administrator even though no user was ever sent — the actor is resolved from the API key.

Request
curl -X GET "https://prod-api.zenadmin.ai/api/external/inventories/archived/4f531f64-7872-403f-b115-3f33f42fd5a7" \
  -H "x-api-key: YOUR_KEY"
200 Response
{
  "success": true,
  "data": {
    "id": "4f531f64-7872-403f-b115-3f33f42fd5a7",
    "serial_number": "QA-20260910-RFQ796",
    "status": "archived",
    "archived_notes": "End of lease",
    "archived_reason": { "value": "sold", "label": "Sold" },
    "archived_details": {
      "sale_price": "450",
      "certificate_number": "WIPE-88213",
      "want_zenadmin_to_dispose": true,
      "need_data_wipe_certificate": true
    },
    "archive_documents": [
      { "file_name": "wipe-certificate.pdf" }
    ],
    "additional_info": {
      "archived_by": {
        "first_name": "Alex",
        "last_name": "Sharma",
        "archived_at": "2026-09-10T09:54:13.959Z"
      }
    }
  }
}

Update Retirement Record

PATCH/api/external/inventories/archived/:id

Partial update. A field you omit keeps its stored value. Send an explicit null to clear one.

Disposal reset: changing to a reason other than sold or waste_disposal clears the six disposal answers — including any you set in the same request. This matches the ZenAdmin console.

Body Parameters
ParameterTypeDescription
archived_reason_iduuidID from /archive-reasons. Nullable
archived_notesstringMax 2000. Nullable
sale_pricestringNullable
certificate_numberstringNullable
want_zenadmin_to_disposebooleanSetting true raises a disposal request
need_data_wipe_certificatebooleanNullable
has_original_invoicebooleanNullable
is_apple_id_lockedbooleanNullable
can_release_from_abmbooleanNullable
can_release_from_autopilotbooleanNullable
archive_documentsobject{ to_add: [], to_remove: [] } — note this differs from the array used when retiring
Request
curl -X PATCH "https://prod-api.zenadmin.ai/api/external/inventories/archived/4f531f64-7872-403f-b115-3f33f42fd5a7" \
  -H "x-api-key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "archived_notes": "Collected by courier" }'
Clearing a Value
curl -X PATCH "https://prod-api.zenadmin.ai/api/external/inventories/archived/4f531f64-7872-403f-b115-3f33f42fd5a7" \
  -H "x-api-key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "sale_price": null }'
200 Response
{
  "success": true,
  "message": "Archived Inventory Updated successfully"
}

Restore Devices

POST/api/external/inventories/archived/restore

Returns retired devices to the active fleet with status unassigned.

Restore discards the archive — it does not undo it. The reason, notes, sale price, certificate number and all disposal answers are cleared, and the attached documents are removed. Read GET /archived/:id first if you need any of it. Re-retiring the device starts from an empty record.

Body Parameters
ParameterTypeDescription
idsarrayRequired. 1–50 device IDs
Request
curl -X POST "https://prod-api.zenadmin.ai/api/external/inventories/archived/restore" \
  -H "x-api-key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "ids": ["4f531f64-7872-403f-b115-3f33f42fd5a7"] }'
200 Response
{
  "success": true,
  "message": "Inventory unarchived successfully"
}

Upload a Document

POST/api/external/inventories/documents/upload-url

Attaching a document such as a data-wipe certificate takes three calls: request a link here, PUT the file to it, then send doc_link back when retiring or updating the record.

signed_url is valid for 60 seconds and is bound to the content type implied by ext — the PUT must send exactly that Content-Type. doc_link is the durable URL you store.

Body Parameters
ParameterTypeDescription
inventoryIduuidRequired. Device the document belongs to
fileNamestringRequired. Letters, digits, spaces and ._()- only
typestringRequired. archive, archive_invoice, invoice, image or others
extstringRequired. pdf, png, jpg, jpeg, csv, webp, docx, xlsx
1. Request a Link
curl -X POST "https://prod-api.zenadmin.ai/api/external/inventories/documents/upload-url" \
  -H "x-api-key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "inventoryId": "4f531f64-7872-403f-b115-3f33f42fd5a7",
    "fileName": "wipe-certificate.pdf",
    "type": "archive",
    "ext": "pdf"
  }'
200 Response
{
  "success": true,
  "data": {
    "signed_url": "https://storage.googleapis.com/...?X-Goog-Signature=...",
    "doc_link": "https://storage.googleapis.com/...",
    "key": "inventory_document/archive/4f531f64/wipe-certificate.pdf",
    "file_name": "wipe-certificate.pdf",
    "expires_in_seconds": 60
  }
}
2. Upload the File
curl -X PUT "SIGNED_URL_FROM_STEP_1" \
  -H "Content-Type: application/pdf" \
  --data-binary @wipe-certificate.pdf
3. Attach to the Record
curl -X PATCH "https://prod-api.zenadmin.ai/api/external/inventories/archived/4f531f64-7872-403f-b115-3f33f42fd5a7" \
  -H "x-api-key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "archive_documents": {
      "to_add": [{
        "doc_link": "DOC_LINK_FROM_STEP_1",
        "file_name": "wipe-certificate.pdf",
        "ext": "pdf",
        "key": "KEY_FROM_STEP_1",
        "size": "20418"
      }]
    }
  }'

Webhooks Overview

ZenAdmin pushes signed JSON to HTTPS URLs you register. Your endpoint must respond with 2xx within 10 seconds. Use the delivery log API to verify connectivity and debug failures — no separate dashboard required.

Debugging tip: After registering a subscription, trigger an order or status change, then call GET /webhooks/deliveries. If no rows appear, check that the subscription is is_active: true and subscribes to the event that was emitted.

Webhook Subscriptions

Create Subscription

POST/api/external/webhooks/subscriptions
FieldRequiredDescription
urlYesHTTPS webhook URL (max 2048 chars)
eventsYesArray of event names (min 1)
secretNoCustom secret (16–128 chars); auto-generated if omitted
Request
curl -X POST "https://prod-api.zenadmin.ai/api/external/webhooks/subscriptions" \
  -H "x-api-key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://acme.atlassian.com/automation/webhook/abc",
    "events": ["order.in_transit", "order.delivered", "order.cancelled"]
  }'
200 Response
{
  "success": true,
  "message": "Subscription created. Save the secret - it will not be shown again.",
  "data": {
    "id": "sub-uuid",
    "url": "https://acme.atlassian.com/automation/webhook/abc",
    "events": ["order.in_transit", "order.delivered", "order.cancelled"],
    "is_active": true,
    "consecutive_failure_count": 0,
    "created_at": "2026-05-08T10:00:00.000Z",
    "updated_at": "2026-05-08T10:00:00.000Z",
    "last_success_at": null,
    "last_failure_at": null,
    "last_failure_reason": null,
    "secret": "a3f1b2c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3"
  }
}

List Subscriptions

GET/api/external/webhooks/subscriptions
200 Response
{
  "success": true,
  "data": [
    {
      "id": "sub-uuid",
      "url": "https://acme.atlassian.com/automation/webhook/abc",
      "events": ["order.in_transit", "order.delivered"],
      "is_active": true,
      "consecutive_failure_count": 0,
      "last_success_at": "2026-05-10T14:30:00.000Z",
      "last_failure_at": null,
      "last_failure_reason": null
    }
  ]
}

Get Subscription

GET/api/external/webhooks/subscriptions/:id
200 Response
{
  "success": true,
  "data": {
    "id": "sub-uuid",
    "url": "https://acme.atlassian.com/automation/webhook/abc",
    "events": ["order.in_transit", "order.delivered"],
    "is_active": true,
    "consecutive_failure_count": 2,
    "last_success_at": "2026-05-10T14:30:00.000Z",
    "last_failure_at": "2026-05-11T09:00:00.000Z",
    "last_failure_reason": "HTTP 500 from endpoint"
  }
}

Update Subscription

PATCH/api/external/webhooks/subscriptions/:id
FieldDescription
urlChange webhook URL
eventsReplace subscribed events
is_activeEnable/disable subscription
rotate_secret: trueGenerate new secret (returned once in response)
secretSet a custom secret (16–128 chars, returned once)
Request
curl -X PATCH "https://prod-api.zenadmin.ai/api/external/webhooks/subscriptions/sub-uuid" \
  -H "x-api-key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"is_active": true, "rotate_secret": true}'

Delete Subscription

DELETE/api/external/webhooks/subscriptions/:id
Request
curl -X DELETE "https://prod-api.zenadmin.ai/api/external/webhooks/subscriptions/sub-uuid" \
  -H "x-api-key: YOUR_KEY"

Webhook Events

Subscribe to one or more events when creating a subscription. Order status changes emit both the specific event (e.g. order.in_transit) and order.status_changed.

EventEmitted when
rfq.createdA partner order request creates an RFQ
rfq.cancelledRFQ cancelled via POST /orders/:rfq_id/cancel
rfq.status_changedRFQ status transition (sentin_progresscompleted / expired)
order.placedOrder item moves to order_placed
order.confirmedOrder item moves to confirmed
order.in_transitOrder item moves to in_transit
order.deliveredOrder item moves to delivered
order.cancelledOrder item moves to cancelled
order.status_changedCatch-all — always emitted alongside the specific order event above
order.tracking_addedReserved — listed in /me but not yet emitted
inventory.archivedOne or more devices retired via POST /inventories/archived
inventory.restoredDevices restored via POST /inventories/archived/restore
inventory.archive_updatedA retirement record changed via PATCH /inventories/archived/:id
disposal.requestedA retire or update asks ZenAdmin to dispose of the device — always emitted alongside the inventory.* event that triggered it, never on its own

Webhook Payloads

Every outbound delivery uses the same envelope. The data object varies by event. rfq_id is null on inventory.* and disposal.requested — those events describe an asset, not an order.

Common envelope
rfq.created
rfq.cancelled
rfq.status_changed
Order events (placed, confirmed, in_transit, delivered, cancelled, status_changed)
inventory.archived
inventory.restored
inventory.archive_updated
disposal.requested
Envelope
{
  "id": "delivery-uuid",
  "event": "order.in_transit",
  "occurred_at": "2026-05-08T15:21:09.000Z",
  "company_id": "99fe5378-d0da-4735-9167-0ba35578e168",
  "rfq_id": 105821,
  "data": { "...event-specific..." }
}
rfq.created data
{
  "rfq_id": 105821,
  "external_ticket_id": "DEV-1234",
  "external_system": "jira",
  "status": "sent"
}
rfq.cancelled data
{
  "rfq_id": 105821,
  "external_ticket_id": "DEV-1234",
  "external_system": "jira",
  "reason": "Employee no longer joining"
}
rfq.status_changed data
{
  "rfq_id": 105821,
  "previous_status": "sent",
  "status": "in_progress",
  "external_ticket_id": "DEV-1234",
  "external_system": "jira",
  "order_id": "ORD-29381"
}
Order event data
{
  "rfq_id": 105821,
  "order_id": "ORD-29381",
  "external_ticket_id": "DEV-1234",
  "external_system": "jira",
  "items": [
    {
      "id": "order-item-uuid",
      "status": "in_transit",
      "previous_status": "confirmed",
      "delivery_at": "2026-05-12T09:00:00.000Z",
      "tracking_number": "1Z999AA10123456784"
    }
  ]
}
inventory.archived data
{
  "inventory_ids": [
    "4f531f64-7872-403f-b115-3f33f42fd5a7",
    "c893374e-d530-4943-a8f2-84c384cec0a5"
  ],
  "archived_by": "Alex Sharma",
  "archived_by_user_id": "0f9c1e77-4c2a-4de1-9b31-2e5a7c81d4aa"
}
inventory.restored data
{
  "inventory_ids": ["4f531f64-7872-403f-b115-3f33f42fd5a7"],
  "restored_by": "Alex Sharma",
  "restored_by_user_id": "0f9c1e77-4c2a-4de1-9b31-2e5a7c81d4aa"
}
inventory.archive_updated data
{
  "inventory_ids": ["4f531f64-7872-403f-b115-3f33f42fd5a7"],
  "updated_by": "Alex Sharma",
  "updated_by_user_id": "0f9c1e77-4c2a-4de1-9b31-2e5a7c81d4aa"
}
disposal.requested data
{
  "inventory_ids": ["4f531f64-7872-403f-b115-3f33f42fd5a7"],
  "requested_by": "Alex Sharma",
  "requested_by_user_id": "0f9c1e77-4c2a-4de1-9b31-2e5a7c81d4aa",
  "need_data_wipe_certificate": true
}

Signature Verification

Every delivery includes these headers. Verify before processing the payload.

HeaderDescription
X-Zenadmin-EventEvent name (e.g. order.in_transit)
X-Zenadmin-Delivery-IdDelivery UUID (matches payload id)
X-Zenadmin-TimestampUnix timestamp used in signature
X-Zenadmin-Signaturesha256=<hex> HMAC signature

Signature = HMAC-SHA256(signing_key, "<timestamp>.<raw_body>") where signing_key is the SHA-256 hex digest of your plaintext subscription secret.

Retries & deactivation

Non-2xx responses or network errors trigger retries: 1m → 5m → 30m → 2h → 12h → 24h (6 attempts total). After 50 consecutive failures the subscription is auto-deactivated — re-enable with PATCH { "is_active": true }.

Node.js
const crypto = require('crypto');

function verify(req, plaintextSecret) {
  const ts = req.header('X-Zenadmin-Timestamp');
  const sig = (req.header('X-Zenadmin-Signature') || '').replace('sha256=', '');
  const signingKey = crypto.createHash('sha256').update(plaintextSecret).digest('hex');
  const expected = crypto
    .createHmac('sha256', signingKey)
    .update(`${ts}.${req.rawBody}`)
    .digest('hex');
  if (expected.length !== sig.length) return false;
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}
Python
import hmac, hashlib

def verify(headers, raw_body: bytes, plaintext_secret: str) -> bool:
    ts = headers['X-Zenadmin-Timestamp']
    sig = headers['X-Zenadmin-Signature'].removeprefix('sha256=')
    signing_key = hashlib.sha256(plaintext_secret.encode()).hexdigest()
    expected = hmac.new(
        signing_key.encode(),
        f"{ts}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, sig)

Delivery Logs

Use delivery logs to confirm ZenAdmin reached your endpoint and to diagnose failures. The response_body_excerpt field contains up to 1KB of your endpoint's response — ZenAdmin does not store personal data in audit fields.

GET/api/external/webhooks/deliveries
Query Parameters
ParameterTypeDescription
subscription_iduuidFilter by subscription
statusstringpending | in_progress | success | failed
eventstringFilter by event name
pageintegerPage number (default 0)
limitintegerMax 100 (default 25)
Redeliver
POST/api/external/webhooks/deliveries/:id/redeliver

Re-enqueues the same payload signed with the current subscription secret. Returns 202.

Debugging webhooks: If no deliveries appear after an order, verify (1) subscription is_active is true, (2) the event is in your subscription's events array, (3) a qualifying business event actually occurred. Use redeliver to re-test signature verification on your endpoint.
List Deliveries
curl -X GET "https://prod-api.zenadmin.ai/api/external/webhooks/deliveries?subscription_id=sub-uuid&status=failed" \
  -H "x-api-key: YOUR_KEY"
200 Response
{
  "success": true,
  "data": [
    {
      "id": "delivery-uuid",
      "subscription_id": "sub-uuid",
      "event": "order.in_transit",
      "status": "failed",
      "attempt": 3,
      "response_status": 500,
      "response_body_excerpt": "Internal Server Error",
      "next_retry_at": "2026-05-08T16:21:09.000Z",
      "created_at": "2026-05-08T15:21:09.000Z",
      "delivered_at": null
    },
    {
      "id": "delivery-uuid-2",
      "subscription_id": "sub-uuid",
      "event": "rfq.created",
      "status": "success",
      "attempt": 1,
      "response_status": 200,
      "response_body_excerpt": "OK",
      "next_retry_at": null,
      "created_at": "2026-05-08T10:00:00.000Z",
      "delivered_at": "2026-05-08T10:00:01.000Z"
    }
  ],
  "meta_data": {
    "total_items": 2,
    "page_no": 0,
    "items_on_page": 2,
    "total_pages": 1,
    "has_next_page": false,
    "has_prev_page": false
  }
}
Redeliver
curl -X POST "https://prod-api.zenadmin.ai/api/external/webhooks/deliveries/delivery-uuid/redeliver" \
  -H "x-api-key: YOUR_KEY"

Automation Example (Jira)

This Jira "Send web request" rule fires POST /orders when a New Hire ticket is created. Status updates flow back via outbound webhooks — ZenAdmin does not call your Jira REST API directly.

SettingValue
MethodPOST
URLhttps://prod-api.zenadmin.ai/api/external/orders
HeadersContent-Type: application/json, x-api-key: {{secret}}

Include both issue key and automation timestamp in external_request_id so retried automations stay idempotent. Register a webhook for order.status_changed pointing at a Jira Incoming webhook to update ticket status from data.items[0].status.

Jira Request Body
{
  "external_request_id": "{{issue.key}}-{{automationStartTime.timestamp}}",
  "external_system": "jira",
  "external_ticket_id": "{{issue.key}}",
  "external_url": "{{issue.url}}",
  "employee": {
    "work_email": "{{issue.fields.customfield_10042}}"
  },
  "ship_to_type": "address",
  "address": {
    "line1": "{{issue.fields.customfield_10101}}",
    "city": "{{issue.fields.customfield_10102}}",
    "country_id": "{{issue.fields.customfield_10103}}",
    "postal_code": "{{issue.fields.customfield_10104}}",
    "contact_number": "{{issue.fields.customfield_10105}}"
  },
  "items": [
    {
      "product_configuration_id": "{{issue.fields.customfield_10110}}"
    }
  ],
  "notes": "{{issue.summary}}"
}