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
Content Type
All requests and responses use application/json.
Timestamps
ISO-8601 UTC (e.g. 2026-05-08T15:21:09.000Z).
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.
- 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.
- Verify connectivity with
GET /me(see curl below). - Browse the catalog:
GET /catalog/countries→ pick acountry_id→GET /catalog/devices?country_id=.... - Create a test order:
POST /orderswith a uniqueexternal_request_id. - Register webhooks:
POST /webhooks/subscriptions— save the returnedsecret(shown once). - Monitor deliveries:
GET /webhooks/deliveries?subscription_id=...to confirm ZenAdmin reached your endpoint. - Retry a failed delivery:
POST /webhooks/deliveries/:id/redeliver(returns 202).
curl -X GET "https://prod-api.zenadmin.ai/api/external/me" \ -H "x-api-key: YOUR_KEY"
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"]
}'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.
| Method | Header | Value |
|---|---|---|
| Recommended | x-api-key | your-api-key |
| Alternative | Authorization | Bearer your-api-key |
| Scope | Limit | Burst |
|---|---|---|
| Global (all endpoints) | 600 req/min per key | 1200 |
POST /orders only | 60 req/min per key | 120 |
{
"success": false,
"message": "Invalid or expired API key"
}{
"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.
| Status | Meaning | When |
|---|---|---|
| 200 | OK | Request succeeded (includes idempotent order replay) |
| 201 | Created | New resource created (e.g. order) |
| 202 | Accepted | Async action queued (e.g. webhook redelivery) |
| 400 | Bad Request | Validation error or missing required field |
| 401 | Unauthorized | Invalid or expired API key |
| 404 | Not Found | Resource not found |
| 409 | Conflict | Business rule violation (not cancellable, duplicate subscription) |
| 422 | Unprocessable | Invalid domain value (country, catalog item) |
| 429 | Too Many Requests | Rate limit exceeded |
| errorType | Status | When |
|---|---|---|
employee_not_found | 404 | No employee matched user_id / work_email / employee_id |
employee_address_missing | 409 | ship_to_type=employee but no personal address on file |
invalid_country_id | 422 | Unknown address.country_id |
invalid_catalog_item | 422 | Unknown product_configuration_id |
missing_external_request_id | 400 | Idempotency key omitted from order create |
order_not_found | 404 | RFQ not found for this company |
order_not_cancellable | 409 | Order already in_transit / delivered / completed |
duplicate_subscription | 409 | Webhook URL already registered |
subscription_not_found | 404 | Webhook subscription ID not found |
delivery_not_found | 404 | Webhook delivery ID not found |
catalog_item_not_found | 404 | Catalog device not found |
rate_limited | 429 | Per-key rate limit hit |
{
"success": false,
"message": "No employee matched work_email",
"errorType": "employee_not_found",
"data": {
"field": "work_email",
"value": "unknown@company.com"
}
}{
"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.
| Field | Description |
|---|---|
total_items | Total matching records |
page_no | Current page (0-based) |
items_on_page | Records in this response |
total_pages | Total pages (catalog, employees, orders, webhook deliveries) |
has_next_page | true if more pages exist |
has_prev_page | true if page > 0 |
GET /devices returns only total_items, page_no, and items_on_page — not the extended pagination fields.{
"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
Returns company-scoped inventory devices.
| Parameter | Type | Description |
|---|---|---|
page | integer | Page number (default 0) |
limit | integer | Page size, max 100 (default 25) |
search | string | Free-text search |
sort_by | string | Sort field |
filter_status | string | Filter by device status |
filter_type | uuid | Filter by product type ID |
filter_brand | uuid | Filter by brand ID |
curl -X GET "https://prod-api.zenadmin.ai/api/external/devices?page=0&limit=25&search=MacBook" \ -H "x-api-key: YOUR_KEY"
{
"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
Returns full details for a single inventory device by UUID.
| Parameter | Type | Description |
|---|---|---|
id | uuid | Device ID |
curl -X GET "https://prod-api.zenadmin.ai/api/external/devices/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \ -H "x-api-key: YOUR_KEY"
{
"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"
}
}
}{
"success": false,
"message": "Device not found"
}Get API Context
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.
curl -X GET "https://prod-api.zenadmin.ai/api/external/me" \ -H "x-api-key: YOUR_KEY"
{
"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
- Idempotent by
external_request_id— replays return 200 - Employee resolution:
user_id→work_email→employee_id external_system:jira|freshservice|atlassian|zendesk|servicenow|customship_to_type:employee(personal address) oraddress(custom block)- Rate limit: 60 req/min (burst 120) on top of global limit
| Field | Required | Description |
|---|---|---|
external_request_id | Yes | Idempotency key (unique per order attempt) |
external_system | Yes | Source system identifier |
external_ticket_id | Yes | Ticket/issue ID in source system |
external_url | No | Link back to source ticket |
employee | Yes | One of user_id, work_email, employee_id |
ship_to_type | Yes | employee or address |
address | If address | line1, city, country_id, postal_code, contact_number |
items | Yes | Array of { product_configuration_id, quantity? } |
notes | No | Free-text notes |
{
"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"
}{
"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
}
}{
"success": true,
"message": "Order already exists for this external_request_id",
"data": { "...same rfq payload..." }
}Get Order
Returns the full RFQ plus derived order items and per-item tracking history. Accepts numeric RFQ ID or internal UUID.
curl -X GET "https://prod-api.zenadmin.ai/api/external/orders/105821" \ -H "x-api-key: YOUR_KEY"
{
"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
| Parameter | Type | Description |
|---|---|---|
page | integer | Page number (default 0) |
limit | integer | Max 100 (default 25) |
status | string | sent | in_progress | completed | expired |
external_system | string | Filter by source system |
external_ticket_id | string | Filter by ticket ID |
from | ISO date | Created on or after |
to | ISO date | Created on or before |
curl -X GET "https://prod-api.zenadmin.ai/api/external/orders?status=in_progress&page=0&limit=25" \ -H "x-api-key: YOUR_KEY"
{
"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
Cancels an RFQ. Emits rfq.cancelled webhook on success.
order_not_cancellable if any order item has reached in_transit or delivered, or the RFQ is already completed.| Field | Type | Description |
|---|---|---|
reason | string | Cancellation reason (included in webhook payload) |
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"}'{
"success": false,
"message": "Order cannot be cancelled in its current state",
"errorType": "order_not_cancellable"
}List Catalog Devices
Browse orderable hardware for a country. Global catalog — not scoped to a department.
| Parameter | Type | Description |
|---|---|---|
country_id | uuid | Required. Country for pricing/availability |
search | string | Search by product name |
product_type | uuid | Filter by product type ID |
brand | uuid | Filter by brand ID |
page | integer | Page number (default 0) |
limit | integer | Max 100 (default 25) |
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"
{
"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
| Parameter | Type | Description |
|---|---|---|
country_id | uuid | Optional — include country-specific price |
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"
{
"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"
}
}{
"success": false,
"message": "Catalog item not found",
"errorType": "catalog_item_not_found"
}List Countries
Returns all countries for catalog lookups. Use the id as country_id in catalog device queries.
curl -X GET "https://prod-api.zenadmin.ai/api/external/catalog/countries" \ -H "x-api-key: YOUR_KEY"
{
"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
Lightweight employee directory for picker UIs and automations.
| Parameter | Type | Description |
|---|---|---|
search | string | Search name, email, or employee ID |
status | string | new | active | offboarding | offboarded | deleted |
department_id | uuid | Filter by department |
page | integer | Page number (default 0) |
limit | integer | Max 100 (default 25) |
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"
{
"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
Returns a single employee by user UUID.
curl -X GET "https://prod-api.zenadmin.ai/api/external/employees/user-uuid" \ -H "x-api-key: YOUR_KEY"
{
"success": false,
"message": "Employee not found",
"errorType": "employee_not_found"
}List Markets
Countries the webstore catalogue can be priced for. Use the id as country_id in the product endpoints below.
| Parameter | Type | Description |
|---|---|---|
search | string | Matches the country name |
page | integer | Page number (default 0) |
limit | integer | Page size, max 100 (default 25) |
curl -X GET "https://prod-api.zenadmin.ai/api/external/webstore/countries?search=sing" \ -H "x-api-key: YOUR_KEY"
{
"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
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.
curl -X GET "https://prod-api.zenadmin.ai/api/external/webstore/products/filters" \ -H "x-api-key: YOUR_KEY"
{
"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
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.
| Parameter | Type | Description |
|---|---|---|
country_id | uuid | Required. Market to price for. Unknown values return 422 invalid_country_id |
search | string | Matches product name, brand and product type — not country |
product_type | uuid | Comma-separated product type IDs |
brand | uuid | Comma-separated brand IDs |
os | uuid | Comma-separated operating system IDs |
processor | string | Value from /products/filters |
ram | string | Value from /products/filters, matched exactly |
storage | string | Value from /products/filters |
display | string | Value from /products/filters |
sort | string | price-asc or price-desc |
page | integer | Page number (default 0) |
limit | integer | Page size, max 100 (default 25) |
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"
{
"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
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.
| Parameter | Type | Description |
|---|---|---|
country_id | uuid | Required. Market to price for |
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"
{
"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
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.
| Parameter | Type | Description |
|---|---|---|
search | string | Matches the reason label |
curl -X GET "https://prod-api.zenadmin.ai/api/external/inventories/archive-reasons" \ -H "x-api-key: YOUR_KEY"
{
"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
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.
| Parameter | Type | Description |
|---|---|---|
archive | array | Required. 1–50 devices |
archive[].id | uuid | Required. Device ID |
archive[].reason | uuid | ID from /archive-reasons |
archive[].notes | string | Free text, max 2000 |
archive[].sale_price | string | Amount realised, if sold |
archive[].certificate_number | string | Data-wipe certificate reference |
archive[].want_zenadmin_to_dispose | boolean | Ask ZenAdmin to collect and dispose |
archive[].need_data_wipe_certificate | boolean | Certificate required on disposal |
archive[].has_original_invoice | boolean | Original invoice available |
archive[].is_apple_id_locked | boolean | Device still tied to an Apple ID |
archive[].can_release_from_abm | boolean | Releasable from Apple Business Manager |
archive[].can_release_from_autopilot | boolean | Releasable from Windows Autopilot |
archive[].archive_documents | array | Documents to attach — see Upload Document |
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
}]
}'{
"success": true,
"message": "Archived successfully"
}List Retired Devices
Devices that have already been retired, with their reason and disposal details. Retired devices are excluded from GET /devices.
| Parameter | Type | Description |
|---|---|---|
search | string | Free-text search |
sort_by | string | Sort field |
sort_order | string | ASC or DESC (default ASC) |
page | integer | Page number (default 0) |
limit | integer | Page size, max 100 (default 25) |
curl -X GET "https://prod-api.zenadmin.ai/api/external/inventories/archived?sort_order=DESC" \ -H "x-api-key: YOUR_KEY"
{
"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
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.
curl -X GET "https://prod-api.zenadmin.ai/api/external/inventories/archived/4f531f64-7872-403f-b115-3f33f42fd5a7" \ -H "x-api-key: YOUR_KEY"
{
"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
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.
| Parameter | Type | Description |
|---|---|---|
archived_reason_id | uuid | ID from /archive-reasons. Nullable |
archived_notes | string | Max 2000. Nullable |
sale_price | string | Nullable |
certificate_number | string | Nullable |
want_zenadmin_to_dispose | boolean | Setting true raises a disposal request |
need_data_wipe_certificate | boolean | Nullable |
has_original_invoice | boolean | Nullable |
is_apple_id_locked | boolean | Nullable |
can_release_from_abm | boolean | Nullable |
can_release_from_autopilot | boolean | Nullable |
archive_documents | object | { to_add: [], to_remove: [] } — note this differs from the array used when retiring |
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" }'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 }'{
"success": true,
"message": "Archived Inventory Updated successfully"
}Restore Devices
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.
| Parameter | Type | Description |
|---|---|---|
ids | array | Required. 1–50 device IDs |
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"] }'{
"success": true,
"message": "Inventory unarchived successfully"
}Upload a Document
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.
| Parameter | Type | Description |
|---|---|---|
inventoryId | uuid | Required. Device the document belongs to |
fileName | string | Required. Letters, digits, spaces and ._()- only |
type | string | Required. archive, archive_invoice, invoice, image or others |
ext | string | Required. pdf, png, jpg, jpeg, csv, webp, docx, xlsx |
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"
}'{
"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
}
}curl -X PUT "SIGNED_URL_FROM_STEP_1" \ -H "Content-Type: application/pdf" \ --data-binary @wipe-certificate.pdf
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.
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
| Field | Required | Description |
|---|---|---|
url | Yes | HTTPS webhook URL (max 2048 chars) |
events | Yes | Array of event names (min 1) |
secret | No | Custom secret (16–128 chars); auto-generated if omitted |
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"]
}'{
"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
{
"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
{
"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
| Field | Description |
|---|---|
url | Change webhook URL |
events | Replace subscribed events |
is_active | Enable/disable subscription |
rotate_secret: true | Generate new secret (returned once in response) |
secret | Set a custom secret (16–128 chars, returned once) |
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
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.
| Event | Emitted when |
|---|---|
rfq.created | A partner order request creates an RFQ |
rfq.cancelled | RFQ cancelled via POST /orders/:rfq_id/cancel |
rfq.status_changed | RFQ status transition (sent → in_progress → completed / expired) |
order.placed | Order item moves to order_placed |
order.confirmed | Order item moves to confirmed |
order.in_transit | Order item moves to in_transit |
order.delivered | Order item moves to delivered |
order.cancelled | Order item moves to cancelled |
order.status_changed | Catch-all — always emitted alongside the specific order event above |
order.tracking_added | Reserved — listed in /me but not yet emitted |
inventory.archived | One or more devices retired via POST /inventories/archived |
inventory.restored | Devices restored via POST /inventories/archived/restore |
inventory.archive_updated | A retirement record changed via PATCH /inventories/archived/:id |
disposal.requested | A 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.
{
"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_id": 105821,
"external_ticket_id": "DEV-1234",
"external_system": "jira",
"status": "sent"
}{
"rfq_id": 105821,
"external_ticket_id": "DEV-1234",
"external_system": "jira",
"reason": "Employee no longer joining"
}{
"rfq_id": 105821,
"previous_status": "sent",
"status": "in_progress",
"external_ticket_id": "DEV-1234",
"external_system": "jira",
"order_id": "ORD-29381"
}{
"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_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_ids": ["4f531f64-7872-403f-b115-3f33f42fd5a7"],
"restored_by": "Alex Sharma",
"restored_by_user_id": "0f9c1e77-4c2a-4de1-9b31-2e5a7c81d4aa"
}{
"inventory_ids": ["4f531f64-7872-403f-b115-3f33f42fd5a7"],
"updated_by": "Alex Sharma",
"updated_by_user_id": "0f9c1e77-4c2a-4de1-9b31-2e5a7c81d4aa"
}{
"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.
| Header | Description |
|---|---|
X-Zenadmin-Event | Event name (e.g. order.in_transit) |
X-Zenadmin-Delivery-Id | Delivery UUID (matches payload id) |
X-Zenadmin-Timestamp | Unix timestamp used in signature |
X-Zenadmin-Signature | sha256=<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.
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 }.
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));
}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.
| Parameter | Type | Description |
|---|---|---|
subscription_id | uuid | Filter by subscription |
status | string | pending | in_progress | success | failed |
event | string | Filter by event name |
page | integer | Page number (default 0) |
limit | integer | Max 100 (default 25) |
Re-enqueues the same payload signed with the current subscription secret. Returns 202.
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.curl -X GET "https://prod-api.zenadmin.ai/api/external/webhooks/deliveries?subscription_id=sub-uuid&status=failed" \ -H "x-api-key: YOUR_KEY"
{
"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
}
}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.
| Setting | Value |
|---|---|
| Method | POST |
| URL | https://prod-api.zenadmin.ai/api/external/orders |
| Headers | Content-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.
{
"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}}"
}